diff --git a/.golangci.yml b/.golangci.yml index f61f267a0f..38f5f89be2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -23,7 +23,7 @@ linters: linters-settings: cyclop: # TODO(sbuerin) fix remaining findings and set to 20 afterwards - max-complexity: 29 + max-complexity: 30 goimports: local-prefixes: sigs.k8s.io/cluster-api-provider-openstack nestif: diff --git a/Makefile b/Makefile index 56b239d632..6358829a6e 100644 --- a/Makefile +++ b/Makefile @@ -96,11 +96,12 @@ PULL_POLICY ?= Always LDFLAGS := $(shell source ./hack/version.sh; version::ldflags) - ## -------------------------------------- ## Testing ## -------------------------------------- +E2E_ARGS ?= + $(ARTIFACTS): mkdir -p $@ @@ -108,15 +109,21 @@ $(ARTIFACTS): test: ## Run tests go test -v ./... +# Can be run manually, e.g. via: +# export OPENSTACK_CLOUD_YAML_FILE="$(pwd)/clouds.yaml" +# E2E_GINKGO_ARGS="-stream -focus='default'" E2E_ARGS="-use-existing-cluster='true'" make test-e2e +E2E_GINKGO_ARGS ?= -stream +.PHONY: test-e2e ## Run e2e tests using clusterctl +test-e2e: $(GINKGO) $(KIND) $(KUSTOMIZE) e2e-image ## Run e2e tests + time $(GINKGO) -trace -progress -v -tags=e2e --nodes=2 $(E2E_GINKGO_ARGS) ./test/e2e/suites/e2e/... -- -config-path="$(E2E_CONF_PATH)" -artifacts-folder="$(ARTIFACTS)" --data-folder="$(E2E_DATA_DIR)" $(E2E_ARGS) + .PHONY: e2e-image e2e-image: docker-pull-prerequisites docker build -f Dockerfile --tag="gcr.io/k8s-staging-capi-openstack/capi-openstack-controller-amd64:e2e" . -E2E_ARGS ?= CONFORMANCE_E2E_ARGS ?= -kubetest.config-file=$(KUBETEST_CONF_PATH) CONFORMANCE_E2E_ARGS += $(E2E_ARGS) CONFORMANCE_GINKGO_ARGS ?= -stream -CONFORMANCE_GINKGO_ARGS += $(GINKGO_ARGS) .PHONY: test-conformance test-conformance: $(GINKGO) $(KIND) $(KUSTOMIZE) e2e-image ## Run clusterctl based conformance test on workload cluster (requires Docker). time $(GINKGO) -trace -progress -v -tags=e2e -focus="conformance" $(CONFORMANCE_GINKGO_ARGS) ./test/e2e/suites/conformance/... -- -config-path="$(E2E_CONF_PATH)" -artifacts-folder="$(ARTIFACTS)" --data-folder="$(E2E_DATA_DIR)" $(CONFORMANCE_E2E_ARGS) diff --git a/controllers/openstackcluster_controller.go b/controllers/openstackcluster_controller.go index b96a990d64..47ab21e9ce 100644 --- a/controllers/openstackcluster_controller.go +++ b/controllers/openstackcluster_controller.go @@ -180,7 +180,6 @@ func reconcileDelete(ctx context.Context, log logr.Logger, client client.Client, } } - // Delete other things if workerSecGroup := openStackCluster.Status.WorkerSecurityGroup; workerSecGroup != nil { log.Info("Deleting worker security group", "name", workerSecGroup.Name) if err = networkingService.DeleteSecurityGroups(workerSecGroup); err != nil { diff --git a/go.mod b/go.mod index 166d9d95ef..13da8d37e4 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( k8s.io/api v0.21.0-beta.0 k8s.io/apimachinery v0.21.0-beta.0 k8s.io/client-go v0.21.0-beta.0 + k8s.io/component-base v0.21.0-beta.0 k8s.io/klog/v2 v2.5.0 k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 sigs.k8s.io/cluster-api v0.3.11-0.20210310224224-a9144a861bf4 diff --git a/hack/ci/devstack-cloud-init.yaml.tpl b/hack/ci/devstack-cloud-init.yaml.tpl index 6131ae0cef..fc83cebd12 100644 --- a/hack/ci/devstack-cloud-init.yaml.tpl +++ b/hack/ci/devstack-cloud-init.yaml.tpl @@ -1,8 +1,8 @@ #cloud-config -hostname: openstack -password: ubuntu -chpasswd: { expire: False } -ssh_pwauth: True +hostname: localhost +users: +- name: ubuntu + lock_passwd: true write_files: - content: | net.ipv4.ip_forward=1 @@ -22,8 +22,11 @@ write_files: # from https://raw.githubusercontent.com/openstack/octavia/master/devstack/contrib/new-octavia-devstack.sh git clone -b stable/${OPENSTACK_RELEASE} https://github.com/openstack/devstack.git /tmp/devstack - cat < /tmp/devstack/localrc + cat < /tmp/devstack/local.conf + + [[local|localrc]] GIT_BASE=https://github.com + HOST_IP=10.0.2.15 # Neutron enable_plugin neutron https://github.com/openstack/neutron stable/${OPENSTACK_RELEASE} @@ -78,12 +81,26 @@ write_files: # Don't download default images, just our test images DOWNLOAD_DEFAULT_IMAGES=False - IMAGE_URLS="https://github.com/sbueringer/image-builder/releases/download/v1.18.15-01/ubuntu-2004-kube-v1.18.15.qcow2," - IMAGE_URLS+="http://download.cirros-cloud.net/0.5.1/cirros-0.5.1-x86_64-disk.img" + # We upload the Amphora image so it doesn't have to be build + IMAGE_URLS="https://github.com/sbueringer/cluster-api-provider-openstack-images/releases/download/amphora-victoria-1/amphora-x64-haproxy.qcow2" + + # See: https://docs.openstack.org/nova/victoria/configuration/sample-config.html + # Helpful commands (on the devstack VM): + # * openstack resource provider list + # * openstack resource provider inventory list 4aa55af2-d50a-4a53-b225-f6b22dd01044 + # * openstack resource provider usage show 4aa55af2-d50a-4a53-b225-f6b22dd01044 + # * openstack hypervisor stats show + # * openstack hypervisor list + # * openstack hypervisor show openstack + # A CPU allocation ratio von 32 gives us 32 vCPUs in devstack + # This should be enough to run multiple e2e tests at the same time + [[post-config|\$NOVA_CONF]] + [DEFAULT] + cpu_allocation_ratio = 32.0 EOF # Create the stack user - /tmp/devstack/tools/create-stack-user.sh + HOST_IP=10.0.2.15 /tmp/devstack/tools/create-stack-user.sh # Move everything into place (/opt/stack is the $HOME folder of the stack user) mv /tmp/devstack /opt/stack/ @@ -95,6 +112,10 @@ write_files: # Add environment variables for auth/endpoints echo 'source /opt/stack/devstack/openrc admin admin' >> /opt/stack/.bashrc + # Upload the images so we don't have to upload them from prow + su - stack -c "source /opt/stack/devstack/openrc admin admin && /opt/stack/devstack/tools/upload_image.sh https://github.com/sbueringer/cluster-api-provider-openstack-images/releases/download/ubuntu-2004-v1.18.15-0/ubuntu-2004-kube-v1.18.15.qcow2" + su - stack -c "source /opt/stack/devstack/openrc admin admin && /opt/stack/devstack/tools/upload_image.sh http://download.cirros-cloud.net/0.5.1/cirros-0.5.1-x86_64-disk.img" + sudo iptables -t nat -I POSTROUTING -o ens4 -s 172.24.4.0/24 -j MASQUERADE sudo iptables -I FORWARD -s 172.24.4.0/24 -j ACCEPT diff --git a/hack/ci/devstack-on-gce-project-install.sh b/hack/ci/devstack-on-gce-project-install.sh index 724133e289..f03103d4e2 100755 --- a/hack/ci/devstack-on-gce-project-install.sh +++ b/hack/ci/devstack-on-gce-project-install.sh @@ -56,7 +56,9 @@ function init_networks() { if [[ ${GCP_NETWORK_NAME} != "default" ]]; then if ! gcloud compute networks describe "${GCP_NETWORK_NAME}" --project "${GCP_PROJECT}" > /dev/null; then - gcloud compute networks create --project "$GCP_PROJECT" "${GCP_NETWORK_NAME}" --subnet-mode auto --quiet + gcloud compute networks create --project "$GCP_PROJECT" "${GCP_NETWORK_NAME}" --subnet-mode custom + gcloud compute networks subnets create "${GCP_NETWORK_NAME}" --project "$GCP_PROJECT" --network="${GCP_NETWORK_NAME}" --range="10.0.0.0/20" --region "${GCP_REGION}" + gcloud compute firewall-rules create "${GCP_NETWORK_NAME}"-allow-http --project "$GCP_PROJECT" \ --allow tcp:80 --network "${GCP_NETWORK_NAME}" --quiet gcloud compute firewall-rules create "${GCP_NETWORK_NAME}"-allow-https --project "$GCP_PROJECT" \ @@ -124,13 +126,13 @@ main() { --project "${GCP_PROJECT}" \ --zone "${GCP_ZONE}" \ --image ubuntu-2004-nested \ - --boot-disk-size 100G \ + --boot-disk-size 300G \ --boot-disk-type pd-ssd \ --can-ip-forward \ --tags http-server,https-server,novnc,openstack-apis \ --min-cpu-platform "${GCP_MACHINE_MIN_CPU_PLATFORM}" \ --machine-type "${GCP_MACHINE_TYPE}" \ - --network-interface=network="${CLUSTER_NAME}-mynetwork,subnet=${CLUSTER_NAME}-mynetwork,aliases=/24" \ + --network-interface="private-network-ip=10.0.2.15,network=${CLUSTER_NAME}-mynetwork,subnet=${CLUSTER_NAME}-mynetwork" \ --metadata-from-file user-data=./hack/ci/devstack-cloud-init.yaml fi @@ -192,6 +194,7 @@ main() { openstack availability zone list openstack domain list + # the flavors are created in a way that we can execute at least 2 e2e tests in parallel (overall we have 32 vCPUs) openstack flavor delete m1.tiny openstack flavor create --ram 512 --disk 1 --vcpus 1 --public --id 1 m1.tiny --property hw_rng:allowed='True' openstack flavor delete m1.small @@ -199,6 +202,10 @@ main() { openstack flavor delete m1.medium openstack flavor create --ram 6144 --disk 10 --vcpus 4 --public --id 3 m1.medium --property hw_rng:allowed='True' + # Adjust the CPU quota + openstack quota set --cores 32 demo + openstack quota set --secgroups 50 demo + export OS_TENANT_NAME=demo export OS_USERNAME=demo export OS_PROJECT_NAME=demo diff --git a/main.go b/main.go index 17c77c6a52..4151600274 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + cliflag "k8s.io/component-base/cli/flag" "k8s.io/klog/v2" "k8s.io/klog/v2/klogr" clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" @@ -120,6 +121,7 @@ func InitFlags(fs *pflag.FlagSet) { func main() { InitFlags(pflag.CommandLine) + pflag.CommandLine.SetNormalizeFunc(cliflag.WordSepNormalizeFunc) pflag.CommandLine.AddGoFlagSet(flag.CommandLine) pflag.Parse() diff --git a/pkg/cloud/services/compute/instance.go b/pkg/cloud/services/compute/instance.go index ec0806d6a1..82ca08b26f 100644 --- a/pkg/cloud/services/compute/instance.go +++ b/pkg/cloud/services/compute/instance.go @@ -234,7 +234,10 @@ func createInstance(is *Service, clusterName string, i *infrav1.Instance) (*infr } if i.Subnet != "" && accessIPv4 == "" { - return nil, fmt.Errorf("no ports with fixed IPs found on Subnet \"%s\"", i.Subnet) + if errd := deletePorts(is, portsList); errd != nil { + return nil, fmt.Errorf("no ports with fixed IPs found on Subnet %q: error cleaning up ports: %v", i.Subnet, errd) + } + return nil, fmt.Errorf("no ports with fixed IPs found on Subnet %q", i.Subnet) } flavorID, err := flavors.IDFromName(is.computeClient, i.Flavor) @@ -266,7 +269,7 @@ func createInstance(is *Service, clusterName string, i *infrav1.Instance) (*infr }).Extract() if err != nil { if errd := deletePorts(is, portsList); errd != nil { - return nil, fmt.Errorf("error recover creating Openstack instance: %v", errd) + return nil, fmt.Errorf("error recover creating Openstack instance: error cleaning up ports: %v", errd) } return nil, fmt.Errorf("error creating Openstack instance: %v", err) } diff --git a/pkg/cloud/services/compute/service.go b/pkg/cloud/services/compute/service.go index 399e0ebd0d..18cd7a310f 100644 --- a/pkg/cloud/services/compute/service.go +++ b/pkg/cloud/services/compute/service.go @@ -37,6 +37,7 @@ type Service struct { logger logr.Logger } +// NewService returns an instance of the compute service. func NewService(client *gophercloud.ProviderClient, clientOpts *clientconfig.ClientOpts, logger logr.Logger) (*Service, error) { identityClient, err := openstack.NewIdentityV3(client, gophercloud.EndpointOpts{ Region: "", @@ -65,16 +66,21 @@ func NewService(client *gophercloud.ProviderClient, clientOpts *clientconfig.Cli if err != nil { return nil, fmt.Errorf("failed to create image service client: %v", err) } - var projectID string - if clientOpts.AuthInfo != nil { - projectID = clientOpts.AuthInfo.ProjectID - if projectID == "" && clientOpts.AuthInfo.ProjectName != "" { - projectID, err = provider.GetProjectID(client, clientOpts.AuthInfo.ProjectName) - if err != nil { - return nil, fmt.Errorf("error retrieveing project id: %v", err) - } + + if clientOpts.AuthInfo == nil { + return nil, fmt.Errorf("failed to get project id: authInfo must be set: %v", err) + } + + projectID := clientOpts.AuthInfo.ProjectID + if projectID == "" && clientOpts.AuthInfo.ProjectName != "" { + projectID, err = provider.GetProjectID(client, clientOpts.AuthInfo.ProjectName) + if err != nil { + return nil, fmt.Errorf("error retrieveing project id: %v", err) } } + if projectID == "" { + return nil, fmt.Errorf("failed to get project id") + } return &Service{ provider: client, diff --git a/pkg/cloud/services/networking/service.go b/pkg/cloud/services/networking/service.go index 4f5e5187df..9851168b98 100644 --- a/pkg/cloud/services/networking/service.go +++ b/pkg/cloud/services/networking/service.go @@ -23,6 +23,8 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" "github.com/gophercloud/utils/openstack/clientconfig" + + "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/provider" ) const ( @@ -32,8 +34,9 @@ const ( // Service interfaces with the OpenStack Networking API. // It will create a network related infrastructure for the cluster, like network, subnet, router, security groups. type Service struct { - client *gophercloud.ServiceClient - logger logr.Logger + projectID string + client *gophercloud.ServiceClient + logger logr.Logger } // NewService returns an instance of the networking service. @@ -44,8 +47,25 @@ func NewService(client *gophercloud.ProviderClient, clientOpts *clientconfig.Cli if err != nil { return nil, fmt.Errorf("failed to create networking service client: %v", err) } + + if clientOpts.AuthInfo == nil { + return nil, fmt.Errorf("failed to get project id: authInfo must be set: %v", err) + } + + projectID := clientOpts.AuthInfo.ProjectID + if projectID == "" && clientOpts.AuthInfo.ProjectName != "" { + projectID, err = provider.GetProjectID(client, clientOpts.AuthInfo.ProjectName) + if err != nil { + return nil, fmt.Errorf("error retrieveing project id: %v", err) + } + } + if projectID == "" { + return nil, fmt.Errorf("failed to get project id") + } + return &Service{ - client: serviceClient, - logger: logger, + projectID: projectID, + client: serviceClient, + logger: logger, }, nil } diff --git a/scripts/ci-e2e.sh b/scripts/ci-e2e.sh new file mode 100755 index 0000000000..ac2042b46d --- /dev/null +++ b/scripts/ci-e2e.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +# Copyright 2021 The Kubernetes Authors. +# +# 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. + +################################################################################ +# usage: ci-conformance.sh +# This program runs the clusterctl conformance e2e tests. +################################################################################ + +set -o nounset +set -o pipefail + +REPO_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. +cd "${REPO_ROOT}" || exit 1 + +# shellcheck source=../hack/ensure-go.sh +source "${REPO_ROOT}/hack/ensure-go.sh" +# shellcheck source=../hack/ensure-kind.sh +source "${REPO_ROOT}/hack/ensure-kind.sh" +# shellcheck source=../hack/ensure-kubectl.sh +source "${REPO_ROOT}/hack/ensure-kubectl.sh" + +RESOURCE_TYPE="${RESOURCE_TYPE:-"gce-project"}" + +ARTIFACTS="${ARTIFACTS:-${PWD}/_artifacts}" +mkdir -p "${ARTIFACTS}/logs/" + +# our exit handler (trap) +cleanup() { + # stop boskos heartbeat + [[ -z ${HEART_BEAT_PID:-} ]] || kill -9 "${HEART_BEAT_PID}" + + # will be started by the devstack installation script + pkill sshuttle +} +trap cleanup EXIT + +#Install requests module explicitly for HTTP calls +python3 -m pip install requests + +# If BOSKOS_HOST is set then acquire a resource of type ${RESOURCE_TYPE} from Boskos. +if [ -n "${BOSKOS_HOST:-}" ]; then + # Check out the account from Boskos and store the produced environment + # variables in a temporary file. + account_env_var_file="$(mktemp)" + python3 hack/boskos.py --get --resource-type="${RESOURCE_TYPE}" 1>"${account_env_var_file}" + checkout_account_status="${?}" + + # If the checkout process was a success then load the account's + # environment variables into this process. + # shellcheck disable=SC1090 + [ "${checkout_account_status}" = "0" ] && . "${account_env_var_file}" + + # Always remove the account environment variable file. It contains + # sensitive information. + rm -f "${account_env_var_file}" + + if [ ! "${checkout_account_status}" = "0" ]; then + echo "error getting account from boskos" 1>&2 + exit "${checkout_account_status}" + fi + + # run the heart beat process to tell boskos that we are still + # using the checked out account periodically + python3 -u hack/boskos.py --heartbeat >> "$ARTIFACTS/logs/boskos.log" 2>&1 & + HEART_BEAT_PID=$! +fi + +"hack/ci/devstack-on-${RESOURCE_TYPE}-install.sh" + +export OPENSTACK_CLOUD_YAML_FILE +OPENSTACK_CLOUD_YAML_FILE="$(pwd)/clouds.yaml" +make test-e2e +test_status="${?}" + +# If Boskos is being used then release the resource back to Boskos. +[ -z "${BOSKOS_HOST:-}" ] || python3 hack/boskos.py --release >> "$ARTIFACTS/logs/boskos.log" 2>&1 + +exit "${test_status}" diff --git a/test/e2e/data/ccm/cloud-controller-manager.yaml b/test/e2e/data/ccm/cloud-controller-manager.yaml new file mode 100644 index 0000000000..2503e47315 --- /dev/null +++ b/test/e2e/data/ccm/cloud-controller-manager.yaml @@ -0,0 +1,240 @@ +# From: https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/master/manifests/controller-manager/openstack-cloud-controller-manager-ds.yaml +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cloud-controller-manager + namespace: kube-system +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: openstack-cloud-controller-manager + namespace: kube-system + labels: + k8s-app: openstack-cloud-controller-manager +spec: + selector: + matchLabels: + k8s-app: openstack-cloud-controller-manager + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + k8s-app: openstack-cloud-controller-manager + spec: + nodeSelector: + node-role.kubernetes.io/master: "" + # we need user root to read the cloud.conf from the host + securityContext: + runAsUser: 0 + tolerations: + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + - key: node-role.kubernetes.io/master + effect: NoSchedule + serviceAccountName: cloud-controller-manager + containers: + - name: openstack-cloud-controller-manager + image: docker.io/k8scloudprovider/openstack-cloud-controller-manager:v1.20.2 + args: + - /bin/openstack-cloud-controller-manager + - --v=1 + - --cloud-config=$(CLOUD_CONFIG) + - --cloud-provider=openstack + - --use-service-account-credentials=true + - --bind-address=127.0.0.1 + volumeMounts: + - mountPath: /etc/kubernetes + name: k8s + readOnly: true + - mountPath: /etc/ssl/certs + name: ca-certs + readOnly: true + resources: + requests: + cpu: 200m + env: + - name: CLOUD_CONFIG + value: /etc/kubernetes/cloud.conf + hostNetwork: true + volumes: + - hostPath: + path: /etc/kubernetes + type: DirectoryOrCreate + name: k8s + - hostPath: + path: /etc/ssl/certs + type: DirectoryOrCreate + name: ca-certs +--- +# From: https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/master/cluster/addons/rbac/cloud-controller-manager-role-bindings.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: system:cloud-node-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:cloud-node-controller +subjects: +- kind: ServiceAccount + name: cloud-node-controller + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: system:pvl-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:pvl-controller +subjects: +- kind: ServiceAccount + name: pvl-controller + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: system:cloud-controller-manager +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:cloud-controller-manager +subjects: +- kind: ServiceAccount + name: cloud-controller-manager + namespace: kube-system +--- +# From: https://raw.githubusercontent.com/kubernetes/cloud-provider-openstack/master/cluster/addons/rbac/cloud-controller-manager-roles.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: system:cloud-controller-manager +rules: +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - create + - update +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - update +- apiGroups: + - "" + resources: + - nodes + verbs: + - '*' +- apiGroups: + - "" + resources: + - nodes/status + verbs: + - patch +- apiGroups: + - "" + resources: + - services + verbs: + - list + - patch + - update + - watch +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - get +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - '*' +- apiGroups: + - "" + resources: + - endpoints + verbs: + - create + - get + - list + - watch + - update +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - list + - get + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: system:cloud-node-controller +rules: +- apiGroups: + - "" + resources: + - nodes + verbs: + - '*' +- apiGroups: + - "" + resources: + - nodes/status + verbs: + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: system:pvl-controller +rules: +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - '*' +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - update diff --git a/test/e2e/data/ci-artifacts-platform-kustomization.yaml b/test/e2e/data/ci-artifacts-platform-kustomization.yaml index d5d51d4b07..797deadb6f 100644 --- a/test/e2e/data/ci-artifacts-platform-kustomization.yaml +++ b/test/e2e/data/ci-artifacts-platform-kustomization.yaml @@ -79,19 +79,6 @@ spec: exit 0 fi - GSUTIL=gsutil - - if ! command -v $${GSUTIL} >/dev/null; then - apt-get update - apt-get install -y apt-transport-https ca-certificates gnupg curl - echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | $${SUDO} tee -a /etc/apt/sources.list.d/google-cloud-sdk.list - curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | $${SUDO} apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - - apt-get update - apt-get install -y google-cloud-sdk - fi - - $${GSUTIL} version - # This test installs release packages or binaries that are a result of the CI and release builds. # It runs '... --version' commands to verify that the binaries are correctly installed # and finally uninstalls the packages. @@ -115,9 +102,10 @@ spec: echo "* testing CI version $${KUBERNETES_VERSION}" # Check for semver if [[ "$${KUBERNETES_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - CI_URL="gs://kubernetes-release/release/$${KUBERNETES_VERSION}/bin/linux/amd64" + CI_URL="https://storage.googleapis.com/kubernetes-release/release/$${KUBERNETES_VERSION}/bin/linux/amd64" VERSION_WITHOUT_PREFIX="$${KUBERNETES_VERSION#v}" - DEBIAN_FRONTEND=noninteractive apt-get install -y apt-transport-https curl + DEBIAN_FRONTEND=noninteractive apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y apt-transport-https ca-certificates curl curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' >/etc/apt/sources.list.d/kubernetes.list apt-get update @@ -129,18 +117,22 @@ spec: DEBIAN_FRONTEND=noninteractive apt-get install -y "$${CI_PACKAGE}=$${PACKAGE_VERSION}" done else - CI_URL="gs://kubernetes-release-dev/ci/$${KUBERNETES_VERSION}/bin/linux/amd64" + CI_URL="https://storage.googleapis.com/kubernetes-release-dev/ci/$${KUBERNETES_VERSION}/bin/linux/amd64" for CI_PACKAGE in "$${PACKAGES_TO_TEST[@]}"; do + # Browser: https://console.cloud.google.com/storage/browser/kubernetes-release-dev?project=kubernetes-release-dev + # e.g.: https://storage.googleapis.com/kubernetes-release-dev/ci/v1.21.0-beta.1.378+cf3374e43491c5/bin/linux/amd64/kubectl echo "* downloading binary: $${CI_URL}/$${CI_PACKAGE}" - $${GSUTIL} cp "$${CI_URL}/$${CI_PACKAGE}" "$${CI_DIR}/$${CI_PACKAGE}" + wget "$${CI_URL}/$${CI_PACKAGE}" -O "$${CI_DIR}/$${CI_PACKAGE}" chmod +x "$${CI_DIR}/$${CI_PACKAGE}" mv "$${CI_DIR}/$${CI_PACKAGE}" "/usr/bin/$${CI_PACKAGE}" done systemctl restart kubelet fi for CI_CONTAINER in "$${CONTAINERS_TO_TEST[@]}"; do + # Browser: https://console.cloud.google.com/storage/browser/kubernetes-release?project=kubernetes-release + # e.g.: https://storage.googleapis.com/kubernetes-release/release/v1.20.4/bin/linux/amd64/kube-apiserver.tar echo "* downloading package: $${CI_URL}/$${CI_CONTAINER}.$${CONTAINER_EXT}" - $${GSUTIL} cp "$${CI_URL}/$${CI_CONTAINER}.$${CONTAINER_EXT}" "$${CI_DIR}/$${CI_CONTAINER}.$${CONTAINER_EXT}" + wget "$${CI_URL}/$${CI_CONTAINER}.$${CONTAINER_EXT}" -O "$${CI_DIR}/$${CI_CONTAINER}.$${CONTAINER_EXT}" $${SUDO} ctr -n k8s.io images import "$${CI_DIR}/$${CI_CONTAINER}.$${CONTAINER_EXT}" || echo "* ignoring expected 'ctr images import' result" $${SUDO} ctr -n k8s.io images tag "k8s.gcr.io/$${CI_CONTAINER}-amd64:$${KUBERNETES_VERSION//+/_}" "k8s.gcr.io/$${CI_CONTAINER}:$${KUBERNETES_VERSION//+/_}" $${SUDO} ctr -n k8s.io images tag "k8s.gcr.io/$${CI_CONTAINER}-amd64:$${KUBERNETES_VERSION//+/_}" "gcr.io/kubernetes-ci-images/$${CI_CONTAINER}:$${KUBERNETES_VERSION//+/_}" @@ -249,19 +241,6 @@ spec: exit 0 fi - GSUTIL=gsutil - - if ! command -v $${GSUTIL} >/dev/null; then - apt-get update - apt-get install -y apt-transport-https ca-certificates gnupg curl - echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | $${SUDO} tee -a /etc/apt/sources.list.d/google-cloud-sdk.list - curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | $${SUDO} apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - - apt-get update - apt-get install -y google-cloud-sdk - fi - - $${GSUTIL} version - # This test installs release packages or binaries that are a result of the CI and release builds. # It runs '... --version' commands to verify that the binaries are correctly installed # and finally uninstalls the packages. @@ -280,14 +259,15 @@ spec: CI_DIR=/tmp/k8s-ci mkdir -p "$${CI_DIR}" declare -a PACKAGES_TO_TEST=("kubectl" "kubelet" "kubeadm") - declare -a CONTAINERS_TO_TEST=("kube-apiserver" "kube-controller-manager" "kube-proxy" "kube-scheduler") + declare -a CONTAINERS_TO_TEST=("kube-proxy") CONTAINER_EXT="tar" echo "* testing CI version $${KUBERNETES_VERSION}" # Check for semver if [[ "$${KUBERNETES_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - CI_URL="gs://kubernetes-release/release/$${KUBERNETES_VERSION}/bin/linux/amd64" + CI_URL="https://storage.googleapis.com/kubernetes-release/release/$${KUBERNETES_VERSION}/bin/linux/amd64" VERSION_WITHOUT_PREFIX="$${KUBERNETES_VERSION#v}" - DEBIAN_FRONTEND=noninteractive apt-get install -y apt-transport-https curl + DEBIAN_FRONTEND=noninteractive apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y apt-transport-https ca-certificates curl curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - echo 'deb https://apt.kubernetes.io/ kubernetes-xenial main' >/etc/apt/sources.list.d/kubernetes.list apt-get update @@ -299,18 +279,22 @@ spec: DEBIAN_FRONTEND=noninteractive apt-get install -y "$${CI_PACKAGE}=$${PACKAGE_VERSION}" done else - CI_URL="gs://kubernetes-release-dev/ci/$${KUBERNETES_VERSION}/bin/linux/amd64" + CI_URL="https://storage.googleapis.com/kubernetes-release-dev/ci/$${KUBERNETES_VERSION}/bin/linux/amd64" for CI_PACKAGE in "$${PACKAGES_TO_TEST[@]}"; do + # Browser: https://console.cloud.google.com/storage/browser/kubernetes-release-dev?project=kubernetes-release-dev + # e.g.: https://storage.googleapis.com/kubernetes-release-dev/ci/v1.21.0-beta.1.378+cf3374e43491c5/bin/linux/amd64/kubectl echo "* downloading binary: $${CI_URL}/$${CI_PACKAGE}" - $${GSUTIL} cp "$${CI_URL}/$${CI_PACKAGE}" "$${CI_DIR}/$${CI_PACKAGE}" + wget "$${CI_URL}/$${CI_PACKAGE}" -O "$${CI_DIR}/$${CI_PACKAGE}" chmod +x "$${CI_DIR}/$${CI_PACKAGE}" mv "$${CI_DIR}/$${CI_PACKAGE}" "/usr/bin/$${CI_PACKAGE}" done systemctl restart kubelet fi for CI_CONTAINER in "$${CONTAINERS_TO_TEST[@]}"; do + # Browser: https://console.cloud.google.com/storage/browser/kubernetes-release?project=kubernetes-release + # e.g.: https://storage.googleapis.com/kubernetes-release/release/v1.20.4/bin/linux/amd64/kube-apiserver.tar echo "* downloading package: $${CI_URL}/$${CI_CONTAINER}.$${CONTAINER_EXT}" - $${GSUTIL} cp "$${CI_URL}/$${CI_CONTAINER}.$${CONTAINER_EXT}" "$${CI_DIR}/$${CI_CONTAINER}.$${CONTAINER_EXT}" + wget "$${CI_URL}/$${CI_CONTAINER}.$${CONTAINER_EXT}" -O "$${CI_DIR}/$${CI_CONTAINER}.$${CONTAINER_EXT}" $${SUDO} ctr -n k8s.io images import "$${CI_DIR}/$${CI_CONTAINER}.$${CONTAINER_EXT}" || echo "* ignoring expected 'ctr images import' result" $${SUDO} ctr -n k8s.io images tag "k8s.gcr.io/$${CI_CONTAINER}-amd64:$${KUBERNETES_VERSION//+/_}" "k8s.gcr.io/$${CI_CONTAINER}:$${KUBERNETES_VERSION//+/_}" $${SUDO} ctr -n k8s.io images tag "k8s.gcr.io/$${CI_CONTAINER}-amd64:$${KUBERNETES_VERSION//+/_}" "gcr.io/kubernetes-ci-images/$${CI_CONTAINER}:$${KUBERNETES_VERSION//+/_}" diff --git a/test/e2e/data/e2e_conf.yaml b/test/e2e/data/e2e_conf.yaml index b55829dffd..81eb34e674 100644 --- a/test/e2e/data/e2e_conf.yaml +++ b/test/e2e/data/e2e_conf.yaml @@ -41,7 +41,7 @@ providers: - old: "imagePullPolicy: Always" new: "imagePullPolicy: IfNotPresent" - old: "--leader-elect" - new: "--leader-elect=false" + new: "--leader-elect=false\n - --sync-period=1m" - name: kubeadm type: BootstrapProvider versions: @@ -54,7 +54,7 @@ providers: - old: "imagePullPolicy: Always" new: "imagePullPolicy: IfNotPresent" - old: "--leader-elect" - new: "--leader-elect=false" + new: "--leader-elect=false\n - --sync-period=1m" - name: kubeadm type: ControlPlaneProvider versions: @@ -67,7 +67,7 @@ providers: - old: "imagePullPolicy: Always" new: "imagePullPolicy: IfNotPresent" - old: "--leader-elect" - new: "--leader-elect=false" + new: "--leader-elect=false\n - --sync-period=1m" - name: openstack type: InfrastructureProvider versions: @@ -85,6 +85,8 @@ providers: new: "imagePullPolicy: IfNotPresent" - old: "--v=2" new: "--v=4" + - old: "--leader-elect" + new: "--leader-elect=false\n - --sync-period=1m" # default variables for the e2e test; those values could be overridden via env variables, thus # allowing the same e2e config file to be re-used in different prow jobs e.g. each one with a K8s version permutation @@ -93,6 +95,7 @@ variables: KUBE_CONTEXT: "kind-capo-e2e" KUBERNETES_VERSION: "v1.20.4" CNI: "../../data/cni/calico.yaml" + CCM: "../../data/ccm/cloud-controller-manager.yaml" EXP_CLUSTER_RESOURCE_SET: "true" OPENSTACK_BASTION_IMAGE_NAME: "cirros-0.5.1-x86_64-disk" OPENSTACK_BASTION_MACHINE_FLAVOR: "m1.tiny" @@ -113,7 +116,6 @@ intervals: conformance/wait-worker-nodes: ["30m", "10s"] default/wait-controllers: ["3m", "10s"] default/wait-cluster: ["20m", "10s"] - default/wait-control-plane: ["20m", "10s"] - default/wait-worker-nodes: ["20m", "10s"] + default/wait-control-plane: ["30m", "10s"] + default/wait-worker-nodes: ["30m", "10s"] default/wait-delete-cluster: ["30m", "10s"] - default/wait-machine-status: ["20m", "10s"] diff --git a/test/e2e/data/infrastructure-openstack/cluster-template-external-cloud-provider.yaml b/test/e2e/data/infrastructure-openstack/cluster-template-external-cloud-provider.yaml index 3ed184ff4d..e439055d3d 100644 --- a/test/e2e/data/infrastructure-openstack/cluster-template-external-cloud-provider.yaml +++ b/test/e2e/data/infrastructure-openstack/cluster-template-external-cloud-provider.yaml @@ -5,6 +5,7 @@ metadata: name: ${CLUSTER_NAME} labels: cni: "${CLUSTER_NAME}-crs-0" + ccm: "${CLUSTER_NAME}-crs-1" spec: clusterNetwork: pods: @@ -164,3 +165,22 @@ spec: resources: - name: "cni-${CLUSTER_NAME}-crs-0" kind: ConfigMap +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: "ccm-${CLUSTER_NAME}-crs-1" +data: ${CCM_RESOURCES} +--- +apiVersion: addons.cluster.x-k8s.io/v1alpha4 +kind: ClusterResourceSet +metadata: + name: "${CLUSTER_NAME}-crs-1" +spec: + strategy: ApplyOnce + clusterSelector: + matchLabels: + ccm: "${CLUSTER_NAME}-crs-1" + resources: + - name: "ccm-${CLUSTER_NAME}-crs-1" + kind: ConfigMap \ No newline at end of file diff --git a/test/e2e/shared/cluster.go b/test/e2e/shared/cluster.go index 5e45ff2474..e73f93d98a 100644 --- a/test/e2e/shared/cluster.go +++ b/test/e2e/shared/cluster.go @@ -45,6 +45,12 @@ func createClusterctlLocalRepository(config *clusterctl.E2EConfig, repositoryFol Expect(cniPath).To(BeAnExistingFile(), "The %s variable should resolve to an existing file", capie2e.CNIPath) createRepositoryInput.RegisterClusterResourceSetConfigMapTransformation(cniPath, capie2e.CNIResources) + // Ensuring a CCM file is defined in the config and register a FileTransformation to inject the referenced file as in place of the CCM_RESOURCES envSubst variable. + Expect(config.Variables).To(HaveKey(CCMPath), "Missing %s variable in the config", CCMPath) + ccmPath := config.GetVariable(CCMPath) + Expect(ccmPath).To(BeAnExistingFile(), "The %s variable should resolve to an existing file", CCMPath) + createRepositoryInput.RegisterClusterResourceSetConfigMapTransformation(ccmPath, CCMResources) + clusterctlConfig := clusterctl.CreateRepository(context.TODO(), createRepositoryInput) Expect(clusterctlConfig).To(BeAnExistingFile(), "The clusterctl config file does not exists in the local repository %s", repositoryFolder) return clusterctlConfig @@ -52,9 +58,34 @@ func createClusterctlLocalRepository(config *clusterctl.E2EConfig, repositoryFol // setupBootstrapCluster installs Cluster API components via clusterctl. func setupBootstrapCluster(config *clusterctl.E2EConfig, scheme *runtime.Scheme, useExistingCluster bool) (bootstrap.ClusterProvider, framework.ClusterProxy) { + Byf("Running setupBootstrapCluster (useExistingCluster: %t)", useExistingCluster) + + // We only want to set clusterProvider if we create a new bootstrap cluster in this test. + // If we re-use an existing one, we don't want to delete it afterwards, so we don't set it. var clusterProvider bootstrap.ClusterProvider - kubeconfigPath := "" - if !useExistingCluster { + var kubeconfigPath string + + // try to use an existing cluster + if useExistingCluster { + // If the kubeContext is locked: try to use the default kubeconfig with the current context + kubeContext := config.GetVariable(KubeContext) + if kubeContext != "" { + testKubeconfigPath := clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() + kubecfg, err := clientcmd.LoadFromFile(testKubeconfigPath) + Expect(err).NotTo(HaveOccurred()) + + // Only use the kubeconfigPath if the current context is the configured kubeContext + // Otherwise we might deploy to the wrong cluster. + // TODO(sbuerin): this logic could be a lot nicer if we could hand over a kubeContext to NewClusterProxy + Byf("Found currentContext %q in %q (configured kubeContext is %q)", kubecfg.CurrentContext, testKubeconfigPath, kubeContext) + if kubecfg.CurrentContext == kubeContext { + kubeconfigPath = testKubeconfigPath + } + } + } + + // If useExistingCluster was false or we couldn't find an existing cluster in the default kubeconfig with the configured kubeContext, let's create a new one + if kubeconfigPath == "" { clusterProvider = bootstrap.CreateKindBootstrapClusterAndLoadImages(context.TODO(), bootstrap.CreateKindBootstrapClusterAndLoadImagesInput{ Name: config.ManagementClusterName, RequiresDockerSock: config.HasDockerProvider(), @@ -66,19 +97,6 @@ func setupBootstrapCluster(config *clusterctl.E2EConfig, scheme *runtime.Scheme, Expect(kubeconfigPath).To(BeAnExistingFile(), "Failed to get the kubeconfig file for the bootstrap cluster") } - // Ensure kubeconfigPath already has been defaulted for the verification below - // If we're not doing it here, it's done inside of framework.NewClusterProxy() - if kubeconfigPath == "" { - kubeconfigPath = clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() - } - - kubeContext := config.GetVariable(KubeContext) - if kubeContext != "" { - kubecfg, err := clientcmd.LoadFromFile(kubeconfigPath) - Expect(err).NotTo(HaveOccurred()) - Expect(kubecfg.CurrentContext).Should(Equal(kubeContext), "current-context of the kubeconfig should be the same as %s (%s)", KubeContext, kubeContext) - } - clusterProxy := framework.NewClusterProxy("bootstrap", kubeconfigPath, scheme) Expect(clusterProxy).ToNot(BeNil(), "Failed to get a bootstrap cluster proxy") diff --git a/test/e2e/shared/common.go b/test/e2e/shared/common.go index be23572bcf..0f09c4f228 100644 --- a/test/e2e/shared/common.go +++ b/test/e2e/shared/common.go @@ -20,10 +20,12 @@ package shared import ( "context" + "encoding/json" "fmt" "os" "path" "path/filepath" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -51,12 +53,16 @@ func SetupSpecNamespace(ctx context.Context, specName string, e2eCtx *E2EContext } func DumpSpecResourcesAndCleanup(ctx context.Context, specName string, namespace *corev1.Namespace, e2eCtx *E2EContext) { - Byf("Dumping all the Cluster API resources in the %q namespace", namespace.Name) + Byf("Running DumpSpecResourcesAndCleanup for namespace %q", namespace.Name) // Dump all Cluster API related resources to artifacts before deleting them. cancelWatches := e2eCtx.Environment.Namespaces[namespace] - DumpSpecResources(ctx, e2eCtx, namespace) + dumpSpecResources(ctx, e2eCtx, namespace) + + dumpOpenStack(ctx, e2eCtx, e2eCtx.Environment.BootstrapClusterProxy.GetName()) + Byf("Dumping all OpenStack server instances in the %q namespace", namespace.Name) - DumpMachines(ctx, e2eCtx, namespace) + dumpMachines(ctx, e2eCtx, namespace) + if !e2eCtx.Settings.SkipCleanup { framework.DeleteAllClustersAndWait(ctx, framework.DeleteAllClustersAndWaitInput{ Client: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), @@ -73,73 +79,83 @@ func DumpSpecResourcesAndCleanup(ctx context.Context, specName string, namespace delete(e2eCtx.Environment.Namespaces, namespace) } -func DumpMachines(ctx context.Context, e2eCtx *E2EContext, namespace *corev1.Namespace) { - By("Running DumpMachines") - cluster := ClusterForSpec(ctx, e2eCtx.Environment.BootstrapClusterProxy, namespace) +func dumpMachines(ctx context.Context, e2eCtx *E2EContext, namespace *corev1.Namespace) { + cluster, err := clusterForSpec(ctx, e2eCtx.Environment.BootstrapClusterProxy, namespace) + if err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "cannot dump machines, couldn't get cluster in namespace %s: %v\n", namespace.Name, err) + return + } if cluster.Status.Bastion == nil || cluster.Status.Bastion.FloatingIP == "" { - _, _ = fmt.Fprintln(GinkgoWriter, "cannot dump machines, cluster doesn't have a bastion host with a floating ip") + _, _ = fmt.Fprintln(GinkgoWriter, "cannot dump machines, cluster doesn't has a bastion host (yet) with a floating ip") return } - machines := MachinesForSpec(ctx, e2eCtx.Environment.BootstrapClusterProxy, namespace) - instances, err := allMachines(ctx, e2eCtx) + machines, err := machinesForSpec(ctx, e2eCtx.Environment.BootstrapClusterProxy, namespace) if err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "cannot dump machines, could not get instances from OpenStack: %v\n", err) + _, _ = fmt.Fprintf(GinkgoWriter, "cannot dump machines, could not get machines: %v\n", err) + return + } + srvs, err := getOpenStackServers(e2eCtx) + if err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "cannot dump machines, could not get servers from OpenStack: %v\n", err) return } - var machineInstance instance for _, m := range machines.Items { - for _, i := range instances { - if i.name == m.Name { - machineInstance = i - break - } - } - if machineInstance.id == "" { - return + srv, ok := srvs[m.Name] + if !ok { + continue } - DumpMachine(ctx, e2eCtx, m, machineInstance, cluster.Status.Bastion.FloatingIP) + dumpMachine(ctx, e2eCtx, m, srv, cluster.Status.Bastion.FloatingIP) } } -func ClusterForSpec(ctx context.Context, clusterProxy framework.ClusterProxy, namespace *corev1.Namespace) *infrav1.OpenStackCluster { +func clusterForSpec(ctx context.Context, clusterProxy framework.ClusterProxy, namespace *corev1.Namespace) (*infrav1.OpenStackCluster, error) { lister := clusterProxy.GetClient() list := new(infrav1.OpenStackClusterList) if err := lister.List(ctx, list, client.InNamespace(namespace.GetName())); err != nil { - _, _ = fmt.Fprintln(GinkgoWriter, "couldn't find cluster") - return nil + return nil, fmt.Errorf("error listing cluster: %v", err) } - Expect(list.Items).To(HaveLen(1), "Expected to find one cluster, found %d", len(list.Items)) - return &list.Items[0] + if len(list.Items) != 1 { + return nil, fmt.Errorf("error expected 1 cluster but got %d: %v", len(list.Items), list.Items) + } + return &list.Items[0], nil } -func MachinesForSpec(ctx context.Context, clusterProxy framework.ClusterProxy, namespace *corev1.Namespace) *infrav1.OpenStackMachineList { - lister := clusterProxy.GetClient() +func machinesForSpec(ctx context.Context, clusterProxy framework.ClusterProxy, namespace *corev1.Namespace) (*infrav1.OpenStackMachineList, error) { list := new(infrav1.OpenStackMachineList) - if err := lister.List(ctx, list, client.InNamespace(namespace.GetName())); err != nil { - _, _ = fmt.Fprintln(GinkgoWriter, "couldn't find machines") - return nil + if err := clusterProxy.GetClient().List(ctx, list, client.InNamespace(namespace.GetName())); err != nil { + return nil, fmt.Errorf("error listing machines: %v", err) } - return list + return list, nil } -func DumpMachine(_ context.Context, e2eCtx *E2EContext, machine infrav1.OpenStackMachine, machineInstance instance, bastionIP string) { +func dumpMachine(ctx context.Context, e2eCtx *E2EContext, machine infrav1.OpenStackMachine, srv server, bastionIP string) { logPath := filepath.Join(e2eCtx.Settings.ArtifactFolder, "clusters", e2eCtx.Environment.BootstrapClusterProxy.GetName()) machineLogBase := path.Join(logPath, "instances", machine.Namespace, machine.Name) metaLog := path.Join(machineLogBase, "instance.log") if err := os.MkdirAll(filepath.Dir(metaLog), 0750); err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "couldn't create directory %q for file: %s", metaLog, err) + _, _ = fmt.Fprintf(GinkgoWriter, "couldn't create directory %q for file: %s\n", metaLog, err) } f, err := os.OpenFile(metaLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "couldn't open file %q: %s", metaLog, err) + _, _ = fmt.Fprintf(GinkgoWriter, "couldn't open file %q: %s\n", metaLog, err) return } defer f.Close() - _, _ = fmt.Fprintf(f, "instance found: %q\n", machineInstance.id) + serverJSON, err := json.MarshalIndent(srv, "", " ") + if err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "error marshalling server %v: %s", srv, err) + } + if err := os.WriteFile(path.Join(machineLogBase, "server.txt"), serverJSON, 0o600); err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "error writing server JSON %s: %s", serverJSON, err) + } + + _, _ = fmt.Fprintf(f, "instance found: %q\n", srv.id) commandsForMachine( + ctx, + e2eCtx.Settings.Debug, f, - machineInstance.ip, + srv.ip, bastionIP, []command{ // don't do this for now, it just takes to long @@ -179,7 +195,7 @@ func DumpMachine(_ context.Context, e2eCtx *E2EContext, machine infrav1.OpenStac ) } -func DumpSpecResources(ctx context.Context, e2eCtx *E2EContext, namespace *corev1.Namespace) { +func dumpSpecResources(ctx context.Context, e2eCtx *E2EContext, namespace *corev1.Namespace) { framework.DumpAllResources(ctx, framework.DumpAllResourcesInput{ Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), Namespace: namespace.Name, @@ -188,7 +204,13 @@ func DumpSpecResources(ctx context.Context, e2eCtx *E2EContext, namespace *corev } func Byf(format string, a ...interface{}) { - By(fmt.Sprintf(format, a...)) + By("[" + time.Now().Format(time.RFC3339) + "] " + fmt.Sprintf(format, a...)) +} + +func Debugf(debug bool, format string, a ...interface{}) { + if debug { + By("[DEBUG] [" + time.Now().Format(time.RFC3339) + "] " + fmt.Sprintf(format, a...)) + } } // LoadE2EConfig loads the e2econfig from the specified path. diff --git a/test/e2e/shared/context.go b/test/e2e/shared/context.go index b2c7fec092..60fc625abe 100644 --- a/test/e2e/shared/context.go +++ b/test/e2e/shared/context.go @@ -75,8 +75,8 @@ type Settings struct { KubetestConfigFilePath string // useCIArtifacts specifies whether or not to use the latest build from the main branch of the Kubernetes repository UseCIArtifacts bool - // SourceTemplate specifies which source template to use - SourceTemplate string + // Debug specifies if the debug log should be logged + Debug bool } // RuntimeEnvironment represents the runtime environment of the test. diff --git a/test/e2e/shared/defaults.go b/test/e2e/shared/defaults.go index e3c96f9b87..136b464c43 100644 --- a/test/e2e/shared/defaults.go +++ b/test/e2e/shared/defaults.go @@ -29,11 +29,19 @@ import ( ) const ( - DefaultSSHKeyPairName = "cluster-api-provider-openstack-sigs-k8s-io" - KubeContext = "KUBE_CONTEXT" - KubernetesVersion = "KUBERNETES_VERSION" - OpenStackCloudYAMLFile = "OPENSTACK_CLOUD_YAML_FILE" - OpenStackCloud = "OPENSTACK_CLOUD" + DefaultSSHKeyPairName = "cluster-api-provider-openstack-sigs-k8s-io" + KubeContext = "KUBE_CONTEXT" + KubernetesVersion = "KUBERNETES_VERSION" + CCMPath = "CCM" + CCMResources = "CCM_RESOURCES" + OpenStackCloudYAMLFile = "OPENSTACK_CLOUD_YAML_FILE" + OpenStackCloud = "OPENSTACK_CLOUD" + OpenStackFailureDomain = "OPENSTACK_FAILURE_DOMAIN" + OpenStackImageName = "OPENSTACK_IMAGE_NAME" + OpenStackNodeMachineFlavor = "OPENSTACK_NODE_MACHINE_FLAVOR" + FlavorDefault = "ci-artifacts" + FlavorWithoutLB = "without-lb-ci-artifacts" + FlavorExternalCloudProvider = "external-cloud-provider-ci-artifacts" ) // DefaultScheme returns the default scheme to use for testing. @@ -53,8 +61,8 @@ func CreateDefaultFlags(ctx *E2EContext) { flag.StringVar(&ctx.Settings.KubetestConfigFilePath, "kubetest.config-file", "", "path to the kubetest configuration file") flag.IntVar(&ctx.Settings.GinkgoNodes, "kubetest.ginkgo-nodes", 1, "number of ginkgo nodes to use") flag.IntVar(&ctx.Settings.GinkgoSlowSpecThreshold, "kubetest.ginkgo-slowSpecThreshold", 120, "time in s before spec is marked as slow") - flag.BoolVar(&ctx.Settings.UseExistingCluster, "use-existing-cluster", false, "if true, the test uses the current cluster instead of creating a new one (default discovery rules apply)") + flag.BoolVar(&ctx.Settings.UseExistingCluster, "use-existing-cluster", false, "if true, the test will try to use an existing cluster and fallback to create a new one if it couldn't be found") flag.BoolVar(&ctx.Settings.SkipCleanup, "skip-cleanup", false, "if true, the resource cleanup after tests will be skipped") flag.StringVar(&ctx.Settings.DataFolder, "data-folder", "", "path to the data folder") - flag.StringVar(&ctx.Settings.SourceTemplate, "source-template", "./infrastructure-openstack/cluster-template.yaml", "path to the data folder") + flag.BoolVar(&ctx.Settings.Debug, "debug", false, "enables the debug log") } diff --git a/test/e2e/shared/exec.go b/test/e2e/shared/exec.go index 3bee091c30..c863b60376 100644 --- a/test/e2e/shared/exec.go +++ b/test/e2e/shared/exec.go @@ -27,73 +27,18 @@ import ( "path" "path/filepath" "strings" + "time" - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack" - "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" . "github.com/onsi/ginkgo" "golang.org/x/crypto/ssh" - - "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/compute" - "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/provider" ) -type instance struct { +type server struct { name string id string ip string } -// allMachines gets all OpenStack servers at once, to save on DescribeInstances -// calls. -func allMachines(_ context.Context, e2eCtx *E2EContext) ([]instance, error) { - openStackCloudYAMLFile := e2eCtx.E2EConfig.GetVariable(OpenStackCloudYAMLFile) - openstackCloud := e2eCtx.E2EConfig.GetVariable(OpenStackCloud) - - clouds := getParsedOpenStackCloudYAML(openStackCloudYAMLFile) - cloud := clouds.Clouds[openstackCloud] - - providerClient, clientOpts, err := provider.NewClient(cloud, nil) - if err != nil { - return nil, fmt.Errorf("error creating provider client: %v", err) - } - - computeClient, err := openstack.NewComputeV2(providerClient, gophercloud.EndpointOpts{Region: clientOpts.RegionName}) - if err != nil { - return nil, fmt.Errorf("error creating compute client: %v", err) - } - - serverListOpts := &servers.ListOpts{} - allPages, err := servers.List(computeClient, serverListOpts).AllPages() - if err != nil { - return nil, fmt.Errorf("error listing server: %v", err) - } - - serverList, err := servers.ExtractServers(allPages) - if err != nil { - return nil, fmt.Errorf("error extracting server: %v", err) - } - - instances := make([]instance, len(serverList)) - for i, server := range serverList { - addrMap, err := compute.GetIPFromInstance(server) - if err != nil { - return nil, fmt.Errorf("error getting ip for server %s: %v", server.Name, err) - } - ip, ok := addrMap["internal"] - if !ok { - return nil, fmt.Errorf("error geting internal ip for server %s: %v", server.Name, err) - } - - instances[i] = instance{ - name: server.Name, - id: server.ID, - ip: ip, - } - } - return instances, nil -} - type command struct { title string cmd string @@ -101,7 +46,7 @@ type command struct { // commandsForMachine opens a terminal connection // and executes the given commands, outputting the results to a file for each. -func commandsForMachine(f *os.File, machineIP, bastionIP string, commands []command) { +func commandsForMachine(ctx context.Context, debug bool, f *os.File, machineIP, bastionIP string, commands []command) { // TODO(sbuerin) try to access via ssh key pair as soon as it's clear how to do that // Issue: https://github.com/kubernetes-sigs/cluster-api-provider-openstack/issues/784 //providerClient, clientOpts, err := getProviderClient(e2eCtx) @@ -116,35 +61,43 @@ func commandsForMachine(f *os.File, machineIP, bastionIP string, commands []comm User: "cirros", Auth: []ssh.AuthMethod{ssh.Password("gocubsgo")}, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }, + Timeout: 10 * time.Second, } cfg.SetDefaults() - - // connect to the bastion host + Debugf(debug, "dialing to bastion host %s", bastionIP) bastionConn, err := ssh.Dial("tcp", fmt.Sprintf("%s:22", bastionIP), cfg) if err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "couldn't connect to bastion host %s: %s", bastionIP, err) + _, _ = fmt.Fprintf(GinkgoWriter, "couldn't connect to bastion host %s: %s\n", bastionIP, err) return } defer bastionConn.Close() // Dial a connection to the service host, from the bastion host + Debugf(debug, "dialing from bastion host %s to machine %s", bastionIP, machineIP) + timeout, timeoutCancel := context.WithTimeout(ctx, 30*time.Second) + defer timeoutCancel() + go func() { + <-timeout.Done() + bastionConn.Close() + }() conn, err := bastionConn.Dial("tcp", fmt.Sprintf("%s:22", machineIP)) if err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "couldn't connect from the bastion host %s to the target instance %s: %s", bastionIP, machineIP, err) + _, _ = fmt.Fprintf(GinkgoWriter, "couldn't connect from the bastion host %s to the target instance %s: %s\n", bastionIP, machineIP, err) return } defer conn.Close() - // connect to the machineInstance via hte bastion host cfg = &ssh.ClientConfig{ User: "capi", Auth: []ssh.AuthMethod{ssh.Password("capi")}, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }, + Timeout: 10 * time.Second, } cfg.SetDefaults() + Debugf(debug, "dialing to machine %s (via tunnel)", machineIP) clientConn, channels, reqs, err := ssh.NewClientConn(conn, machineIP, cfg) if err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "couldn't connect from local to the target instance %s: %s", machineIP, err) + _, _ = fmt.Fprintf(GinkgoWriter, "couldn't connect from local to the target instance %s: %s\n", machineIP, err) return } defer clientConn.Close() @@ -152,9 +105,10 @@ func commandsForMachine(f *os.File, machineIP, bastionIP string, commands []comm sshClient := ssh.NewClient(clientConn, channels, reqs) for _, c := range commands { + Debugf(debug, "executing cmd %q on machine %s", c.cmd, machineIP) session, err := sshClient.NewSession() if err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "couldn't open session from local to the target instance %s: %s", machineIP, err) + _, _ = fmt.Fprintf(GinkgoWriter, "couldn't open session from local to the target instance %s: %s\n", machineIP, err) continue } defer session.Close() @@ -170,8 +124,9 @@ func commandsForMachine(f *os.File, machineIP, bastionIP string, commands []comm } result := strings.TrimSuffix(stdoutBuf.String(), "\n") + "\n" + strings.TrimSuffix(stderrBuf.String(), "\n") if err := os.WriteFile(logFile, []byte(result), os.ModePerm); err != nil { - _, _ = fmt.Fprintf(f, "error writing log file: %s", err) + _, _ = fmt.Fprintf(f, "error writing log file: %s\n", err) continue } + Debugf(debug, "finished executing cmd %q on machine %s", c.cmd, machineIP) } } diff --git a/test/e2e/shared/exec_test.go b/test/e2e/shared/exec_test.go index 793260e6c7..edd13a892c 100644 --- a/test/e2e/shared/exec_test.go +++ b/test/e2e/shared/exec_test.go @@ -19,6 +19,7 @@ limitations under the License. package shared import ( + "context" "os" "testing" ) @@ -87,7 +88,7 @@ func Test_commandsForMachine(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - commandsForMachine(f, tt.args.machineIP, tt.args.bastionIP, tt.args.commands) + commandsForMachine(context.Background(), true, f, tt.args.machineIP, tt.args.bastionIP, tt.args.commands) }) } } diff --git a/test/e2e/shared/openstack.go b/test/e2e/shared/openstack.go index f34b9a1593..65961b286c 100644 --- a/test/e2e/shared/openstack.go +++ b/test/e2e/shared/openstack.go @@ -40,24 +40,22 @@ import ( "gopkg.in/ini.v1" "sigs.k8s.io/yaml" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/compute" "sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/provider" ) // ensureSSHKeyPair ensures A SSH key is present under the name. -func ensureSSHKeyPair(openStackCloudYAMLFile, cloudName, keyPairName string) { - Byf("Ensuring presence of SSH key in OpenStack: key-name=%s", keyPairName) +func ensureSSHKeyPair(e2eCtx *E2EContext) { + Byf("Ensuring presence of SSH key %q in OpenStack", DefaultSSHKeyPairName) - clouds := getParsedOpenStackCloudYAML(openStackCloudYAMLFile) - cloud := clouds.Clouds[cloudName] - - providerClient, clientOpts, err := provider.NewClient(cloud, nil) + providerClient, clientOpts, err := getProviderClient(e2eCtx) Expect(err).NotTo(HaveOccurred()) computeClient, err := openstack.NewComputeV2(providerClient, gophercloud.EndpointOpts{Region: clientOpts.RegionName}) Expect(err).NotTo(HaveOccurred()) keyPairCreateOpts := &keypairs.CreateOpts{ - Name: keyPairName, + Name: DefaultSSHKeyPairName, } _, err = keypairs.Create(computeClient, keyPairCreateOpts).Extract() if err != nil && !strings.Contains(err.Error(), "already exists") { @@ -65,8 +63,8 @@ func ensureSSHKeyPair(openStackCloudYAMLFile, cloudName, keyPairName string) { } } -func DumpOpenStackClusters(_ context.Context, e2eCtx *E2EContext, bootstrapClusterProxyName string) { - By("Running DumpOpenStackClusters") +func dumpOpenStack(_ context.Context, e2eCtx *E2EContext, bootstrapClusterProxyName string) { + Byf("Running dumpOpenStack") logPath := filepath.Join(e2eCtx.Settings.ArtifactFolder, "clusters", bootstrapClusterProxyName, "openstack-resources") if err := os.MkdirAll(logPath, os.ModePerm); err != nil { _, _ = fmt.Fprintf(GinkgoWriter, "error creating directory %s: %s\n", logPath, err) @@ -80,41 +78,11 @@ func DumpOpenStackClusters(_ context.Context, e2eCtx *E2EContext, bootstrapClust return } - if err := dumpOpenStackServers(providerClient, clientOpts, logPath); err != nil { - _, _ = fmt.Fprintf(GinkgoWriter, "error dumping OpenStack server: %s\n", err) - } - if err := dumpOpenStackImages(providerClient, clientOpts, logPath); err != nil { _, _ = fmt.Fprintf(GinkgoWriter, "error dumping OpenStack images: %s\n", err) } } -func dumpOpenStackServers(providerClient *gophercloud.ProviderClient, clientOpts *clientconfig.ClientOpts, logPath string) error { - computeClient, err := openstack.NewComputeV2(providerClient, gophercloud.EndpointOpts{ - Region: clientOpts.RegionName, - }) - if err != nil { - return fmt.Errorf("error creating compute client: %s", err) - } - - allPages, err := servers.List(computeClient, servers.ListOpts{}).AllPages() - if err != nil { - return fmt.Errorf("error getting server: %s", err) - } - serverList, err := servers.ExtractServers(allPages) - if err != nil { - return fmt.Errorf("error extracting server: %s", err) - } - serverJSON, err := json.MarshalIndent(serverList, "", " ") - if err != nil { - return fmt.Errorf("error marshalling server %v: %s", serverList, err) - } - if err := os.WriteFile(path.Join(logPath, "server.txt"), serverJSON, 0o600); err != nil { - return fmt.Errorf("error writing severJSON %s: %s", serverJSON, err) - } - return nil -} - func dumpOpenStackImages(providerClient *gophercloud.ProviderClient, clientOpts *clientconfig.ClientOpts, logPath string) error { imageClient, err := openstack.NewImageServiceV2(providerClient, gophercloud.EndpointOpts{ Region: clientOpts.RegionName, @@ -141,6 +109,52 @@ func dumpOpenStackImages(providerClient *gophercloud.ProviderClient, clientOpts return nil } +// getOpenStackServers gets all OpenStack servers at once, to save on DescribeInstances +// calls. +func getOpenStackServers(e2eCtx *E2EContext) (map[string]server, error) { + providerClient, clientOpts, err := getProviderClient(e2eCtx) + if err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "error creating provider client: %s\n", err) + return nil, nil + } + + computeClient, err := openstack.NewComputeV2(providerClient, gophercloud.EndpointOpts{Region: clientOpts.RegionName}) + if err != nil { + return nil, fmt.Errorf("error creating compute client: %v", err) + } + + serverListOpts := &servers.ListOpts{} + allPages, err := servers.List(computeClient, serverListOpts).AllPages() + if err != nil { + return nil, fmt.Errorf("error listing server: %v", err) + } + + serverList, err := servers.ExtractServers(allPages) + if err != nil { + return nil, fmt.Errorf("error extracting server: %v", err) + } + + srvs := map[string]server{} + for _, srv := range serverList { + addrMap, err := compute.GetIPFromInstance(srv) + if err != nil { + return nil, fmt.Errorf("error getting ip for server %s: %v", srv.Name, err) + } + ip, ok := addrMap["internal"] + if !ok { + _, _ = fmt.Fprintf(GinkgoWriter, "error getting internal ip for server %s: internal ip doesn't exist (yet)\n", srv.Name) + continue + } + + srvs[srv.Name] = server{ + name: srv.Name, + id: srv.ID, + ip: ip, + } + } + return srvs, nil +} + func getProviderClient(e2eCtx *E2EContext) (*gophercloud.ProviderClient, *clientconfig.ClientOpts, error) { openStackCloudYAMLFile := e2eCtx.E2EConfig.GetVariable(OpenStackCloudYAMLFile) openstackCloud := e2eCtx.E2EConfig.GetVariable(OpenStackCloud) diff --git a/test/e2e/shared/openstack_test.go b/test/e2e/shared/openstack_test.go index b7c9451331..37cd7c53a3 100644 --- a/test/e2e/shared/openstack_test.go +++ b/test/e2e/shared/openstack_test.go @@ -40,7 +40,7 @@ func Test_dumpOpenStackClusters(t *testing.T) { t.Fatal(err) } - DumpOpenStackClusters(context.TODO(), &E2EContext{ + dumpOpenStack(context.TODO(), &E2EContext{ E2EConfig: &clusterctl.E2EConfig{ Variables: map[string]string{ OpenStackCloudYAMLFile: fmt.Sprintf("%s/../../../clouds.yaml", currentDir), diff --git a/test/e2e/shared/suite.go b/test/e2e/shared/suite.go index a827d33e20..f1f111f92b 100644 --- a/test/e2e/shared/suite.go +++ b/test/e2e/shared/suite.go @@ -21,9 +21,13 @@ package shared import ( "context" "flag" + "fmt" + "io/fs" "io/ioutil" "os" + "path" "path/filepath" + "strings" "time" . "github.com/onsi/ginkgo" @@ -47,65 +51,83 @@ type synchronizedBeforeTestSuiteConfig struct { // Node1BeforeSuite is the common setup down on the first ginkgo node before the test suite runs. func Node1BeforeSuite(e2eCtx *E2EContext) []byte { - By("Running Node1BeforeSuite") + Byf("Running Node1BeforeSuite") + defer Byf("Finished Node1BeforeSuite") flag.Parse() Expect(e2eCtx.Settings.ConfigPath).To(BeAnExistingFile(), "Invalid test suite argument. configPath should be an existing file.") Expect(os.MkdirAll(e2eCtx.Settings.ArtifactFolder, 0o750)).To(Succeed(), "Invalid test suite argument. Can't create artifacts-folder %q", e2eCtx.Settings.ArtifactFolder) Byf("Loading the e2e test configuration from %q", e2eCtx.Settings.ConfigPath) e2eCtx.E2EConfig = LoadE2EConfig(e2eCtx.Settings.ConfigPath) - sourceTemplate, err := ioutil.ReadFile(filepath.Join(e2eCtx.Settings.DataFolder, e2eCtx.Settings.SourceTemplate)) - Expect(err).NotTo(HaveOccurred()) - - var clusterctlCITemplate clusterctl.Files - platformKustomization, err := ioutil.ReadFile(filepath.Join(e2eCtx.Settings.DataFolder, "ci-artifacts-platform-kustomization.yaml")) - Expect(err).NotTo(HaveOccurred()) + Expect(e2eCtx.E2EConfig.GetVariable(OpenStackCloudYAMLFile)).To(BeAnExistingFile(), "Invalid test suite argument. Value of environment variable OPENSTACK_CLOUD_YAML_FILE should be an existing file: %s", e2eCtx.E2EConfig.GetVariable(OpenStackCloudYAMLFile)) + Byf("Loading the clouds.yaml from %q", e2eCtx.E2EConfig.GetVariable(OpenStackCloudYAMLFile)) - // TODO(sbuerin): should be removed after: https://github.com/kubernetes-sigs/kustomize/issues/2825 is fixed - //ciTemplate, err := kubernetesversions.GenerateCIArtifactsInjectedTemplateForDebian( - // kubernetesversions.GenerateCIArtifactsInjectedTemplateForDebianInput{ - // ArtifactsDirectory: e2eCtx.Settings.ArtifactFolder, - // SourceTemplate: sourceTemplate, - // PlatformKustomization: platformKustomization, - // }, - //) - ciTemplate, err := GenerateCIArtifactsInjectedTemplateForDebian( - GenerateCIArtifactsInjectedTemplateForDebianInput{ - ArtifactsDirectory: e2eCtx.Settings.ArtifactFolder, - SourceTemplate: sourceTemplate, - PlatformKustomization: platformKustomization, - }, - ) - Expect(err).NotTo(HaveOccurred()) + // TODO(sbuerin): we always need ci artifacts, because we don't have images for every Kubernetes version + err := filepath.WalkDir(path.Join(e2eCtx.Settings.DataFolder, "infrastructure-openstack"), func(f string, d fs.DirEntry, _ error) error { + filename := filepath.Base(f) + fileExtension := filepath.Ext(filename) + if d.IsDir() || !strings.HasPrefix(filename, "cluster-template") { + return nil + } - clusterctlCITemplate = clusterctl.Files{ - SourcePath: ciTemplate, - TargetName: "cluster-template-conformance-ci-artifacts.yaml", - } + sourceTemplate, err := ioutil.ReadFile(f) + Expect(err).NotTo(HaveOccurred()) + + platformKustomization, err := ioutil.ReadFile(filepath.Join(e2eCtx.Settings.DataFolder, "ci-artifacts-platform-kustomization.yaml")) + Expect(err).NotTo(HaveOccurred()) + + // TODO(sbuerin): should be removed after: https://github.com/kubernetes-sigs/kustomize/issues/2825 is fixed + //ciTemplate, err := kubernetesversions.GenerateCIArtifactsInjectedTemplateForDebian( + // kubernetesversions.GenerateCIArtifactsInjectedTemplateForDebianInput{ + // ArtifactsDirectory: e2eCtx.Settings.ArtifactFolder, + // SourceTemplate: sourceTemplate, + // PlatformKustomization: platformKustomization, + // }, + //) + ciTemplate, err := GenerateCIArtifactsInjectedTemplateForDebian( + GenerateCIArtifactsInjectedTemplateForDebianInput{ + ArtifactsDirectory: e2eCtx.Settings.ArtifactFolder, + SourceTemplate: sourceTemplate, + PlatformKustomization: platformKustomization, + }, + ) + Expect(err).NotTo(HaveOccurred()) + + targetName := fmt.Sprintf("%s-ci-artifacts.yaml", strings.TrimSuffix(filename, fileExtension)) + targetTemplate := path.Join(e2eCtx.Settings.ArtifactFolder, "templates", targetName) + + // We have to copy the file from ciTemplate to targetTemplate. Otherwise it would be overwritten because + // ciTemplate is the same for all templates + ciTemplateBytes, err := os.ReadFile(ciTemplate) + Expect(err).NotTo(HaveOccurred()) + err = os.WriteFile(targetTemplate, ciTemplateBytes, 0o600) + Expect(err).NotTo(HaveOccurred()) + + clusterctlCITemplate := clusterctl.Files{ + SourcePath: targetTemplate, + TargetName: targetName, + } - providers := e2eCtx.E2EConfig.Providers - for i, prov := range providers { - if prov.Name != "openstack" { - continue + for i, prov := range e2eCtx.E2EConfig.Providers { + if prov.Name != "openstack" { + continue + } + e2eCtx.E2EConfig.Providers[i].Files = append(e2eCtx.E2EConfig.Providers[i].Files, clusterctlCITemplate) } - e2eCtx.E2EConfig.Providers[i].Files = append(e2eCtx.E2EConfig.Providers[i].Files, clusterctlCITemplate) - } + return nil + }) + Expect(err).NotTo(HaveOccurred()) - openStackCloudYAMLFile := e2eCtx.E2EConfig.GetVariable(OpenStackCloudYAMLFile) - openStackCloud := e2eCtx.E2EConfig.GetVariable(OpenStackCloud) - ensureSSHKeyPair(openStackCloudYAMLFile, openStackCloud, DefaultSSHKeyPairName) + ensureSSHKeyPair(e2eCtx) Byf("Creating a clusterctl local repository into %q", e2eCtx.Settings.ArtifactFolder) e2eCtx.Environment.ClusterctlConfigPath = createClusterctlLocalRepository(e2eCtx.E2EConfig, filepath.Join(e2eCtx.Settings.ArtifactFolder, "repository")) - By("Setting up the bootstrap cluster") + Byf("Setting up the bootstrap cluster") e2eCtx.Environment.BootstrapClusterProvider, e2eCtx.Environment.BootstrapClusterProxy = setupBootstrapCluster(e2eCtx.E2EConfig, e2eCtx.Environment.Scheme, e2eCtx.Settings.UseExistingCluster) - SetEnvVar("OPENSTACK_CLOUD_YAML_B64", getEncodedOpenStackCloudYAML(openStackCloudYAMLFile), true) - SetEnvVar("OPENSTACK_CLOUD_PROVIDER_CONF_B64", getEncodedOpenStackCloudProviderConf(openStackCloudYAMLFile, openStackCloud), true) - - By("Initializing the bootstrap cluster") + Byf("Initializing the bootstrap cluster") initBootstrapCluster(e2eCtx) conf := synchronizedBeforeTestSuiteConfig{ @@ -127,7 +149,8 @@ func Node1BeforeSuite(e2eCtx *E2EContext) []byte { // AllNodesBeforeSuite is the common setup down on each ginkgo parallel node before the test suite runs. func AllNodesBeforeSuite(e2eCtx *E2EContext, data []byte) { - By("Running AllNodesBeforeSuite") + Byf("Running AllNodesBeforeSuite") + defer Byf("Finished AllNodesBeforeSuite") conf := &synchronizedBeforeTestSuiteConfig{} err := yaml.UnmarshalStrict(data, conf) @@ -142,28 +165,40 @@ func AllNodesBeforeSuite(e2eCtx *E2EContext, data []byte) { e2eCtx.Settings.GinkgoNodes = conf.GinkgoNodes e2eCtx.Settings.GinkgoSlowSpecThreshold = conf.GinkgoSlowSpecThreshold + openStackCloudYAMLFile := e2eCtx.E2EConfig.GetVariable(OpenStackCloudYAMLFile) + openStackCloud := e2eCtx.E2EConfig.GetVariable(OpenStackCloud) + SetEnvVar("OPENSTACK_CLOUD_YAML_B64", getEncodedOpenStackCloudYAML(openStackCloudYAMLFile), true) + SetEnvVar("OPENSTACK_CLOUD_PROVIDER_CONF_B64", getEncodedOpenStackCloudProviderConf(openStackCloudYAMLFile, openStackCloud), true) SetEnvVar("OPENSTACK_SSH_KEY_NAME", DefaultSSHKeyPairName, false) - e2eCtx.Environment.ResourceTicker = time.NewTicker(time.Second * 5) + e2eCtx.Environment.ResourceTicker = time.NewTicker(time.Second * 10) e2eCtx.Environment.ResourceTickerDone = make(chan bool) // Get OpenStack server logs every 5 minutes - e2eCtx.Environment.MachineTicker = time.NewTicker(time.Second * 300) + e2eCtx.Environment.MachineTicker = time.NewTicker(time.Second * 60) e2eCtx.Environment.MachineTickerDone = make(chan bool) resourceCtx, resourceCancel := context.WithCancel(context.Background()) machineCtx, machineCancel := context.WithCancel(context.Background()) + debug := e2eCtx.Settings.Debug // Dump resources every 5 seconds go func() { defer GinkgoRecover() for { select { case <-e2eCtx.Environment.ResourceTickerDone: + Debugf(debug, "Running ResourceTickerDone") resourceCancel() + Debugf(debug, "Finished ResourceTickerDone") return case <-e2eCtx.Environment.ResourceTicker.C: + Debugf(debug, "Running ResourceTicker") for k := range e2eCtx.Environment.Namespaces { - DumpSpecResources(resourceCtx, e2eCtx, k) + // ensure dumpSpecResources cannot get stuck indefinitely + timeoutCtx, timeoutCancel := context.WithTimeout(resourceCtx, time.Second*5) + dumpSpecResources(timeoutCtx, e2eCtx, k) + timeoutCancel() } + Debugf(debug, "Finished ResourceTicker") } } }() @@ -174,41 +209,60 @@ func AllNodesBeforeSuite(e2eCtx *E2EContext, data []byte) { for { select { case <-e2eCtx.Environment.MachineTickerDone: + Debugf(debug, "Running MachineTickerDone") machineCancel() + Debugf(debug, "Finished MachineTickerDone") return case <-e2eCtx.Environment.MachineTicker.C: + Debugf(debug, "Running MachineTicker") for k := range e2eCtx.Environment.Namespaces { - DumpMachines(machineCtx, e2eCtx, k) + // ensure dumpMachines cannot get stuck indefinitely + timeoutCtx, timeoutCancel := context.WithTimeout(machineCtx, time.Second*30) + dumpMachines(timeoutCtx, e2eCtx, k) + timeoutCancel() } + Debugf(debug, "Finished MachineTicker") } } }() } -// Node1AfterSuite is cleanup that runs on the first ginkgo node after the test suite finishes. -func Node1AfterSuite(e2eCtx *E2EContext) { - By("Running Node1AfterSuite") +// AllNodesAfterSuite is cleanup that runs on all ginkgo parallel nodes after the test suite finishes. +func AllNodesAfterSuite(e2eCtx *E2EContext) { + Byf("Running AllNodesAfterSuite") + defer Byf("Finished AllNodesAfterSuite") + Byf("Stopping ResourceTicker") if e2eCtx.Environment.ResourceTickerDone != nil { e2eCtx.Environment.ResourceTickerDone <- true } + Byf("Stopped ResourceTicker") + + Byf("Stopping MachineTicker") if e2eCtx.Environment.MachineTickerDone != nil { e2eCtx.Environment.MachineTickerDone <- true } + Byf("Stopped MachineTicker") + ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Minute) defer cancel() - DumpOpenStackClusters(ctx, e2eCtx, e2eCtx.Environment.BootstrapClusterProxy.GetName()) - for k := range e2eCtx.Environment.Namespaces { - DumpSpecResourcesAndCleanup(ctx, "", k, e2eCtx) - DumpMachines(ctx, e2eCtx, k) + + if e2eCtx.Environment.BootstrapClusterProxy == nil { + _, _ = fmt.Fprintf(GinkgoWriter, "bootstrap cluster proxy does not exist yet, cannot dump clusters and machines\n") + return + } + + for namespace := range e2eCtx.Environment.Namespaces { + DumpSpecResourcesAndCleanup(ctx, "after-suite", namespace, e2eCtx) } } -// AllNodesAfterSuite is cleanup that runs on all ginkgo parallel nodes after the test suite finishes. -func AllNodesAfterSuite(e2eCtx *E2EContext) { - By("Running AllNodesAfterSuite") +// Node1AfterSuite is cleanup that runs on the first ginkgo node after the test suite finishes. +func Node1AfterSuite(e2eCtx *E2EContext) { + Byf("Running Node1AfterSuite") + defer Byf("Finished Node1AfterSuite") - By("Tearing down the management cluster") + Byf("Tearing down the management cluster") if !e2eCtx.Settings.SkipCleanup { tearDown(e2eCtx.Environment.BootstrapClusterProvider, e2eCtx.Environment.BootstrapClusterProxy) } diff --git a/test/e2e/suites/conformance/conformance_suite_test.go b/test/e2e/suites/conformance/conformance_suite_test.go index 99eea7a482..3266695363 100644 --- a/test/e2e/suites/conformance/conformance_suite_test.go +++ b/test/e2e/suites/conformance/conformance_suite_test.go @@ -35,9 +35,9 @@ func init() { shared.CreateDefaultFlags(e2eCtx) } -func TestE2EConformance(t *testing.T) { +func TestConformance(t *testing.T) { RegisterFailHandler(Fail) - RunSpecsWithDefaultAndCustomReporters(t, "capo-e2e-conformance", []Reporter{framework.CreateJUnitReporterForProw(e2eCtx.Settings.ArtifactFolder)}) + RunSpecsWithDefaultAndCustomReporters(t, "capo-conformance", []Reporter{framework.CreateJUnitReporterForProw(e2eCtx.Settings.ArtifactFolder)}) } var _ = SynchronizedBeforeSuite(func() []byte { @@ -47,7 +47,7 @@ var _ = SynchronizedBeforeSuite(func() []byte { }) var _ = SynchronizedAfterSuite(func() { - shared.Node1AfterSuite(e2eCtx) -}, func() { shared.AllNodesAfterSuite(e2eCtx) +}, func() { + shared.Node1AfterSuite(e2eCtx) }) diff --git a/test/e2e/suites/conformance/conformance_test.go b/test/e2e/suites/conformance/conformance_test.go index 3ee06dde8c..35cf57e29c 100644 --- a/test/e2e/suites/conformance/conformance_test.go +++ b/test/e2e/suites/conformance/conformance_test.go @@ -31,7 +31,6 @@ import ( "sigs.k8s.io/cluster-api/test/framework/clusterctl" "sigs.k8s.io/cluster-api/test/framework/kubernetesversions" "sigs.k8s.io/cluster-api/test/framework/kubetest" - "sigs.k8s.io/cluster-api/util" "sigs.k8s.io/cluster-api-provider-openstack/test/e2e/shared" ) @@ -52,15 +51,12 @@ var _ = Describe("conformance tests", func() { namespace = shared.SetupSpecNamespace(ctx, specName, e2eCtx) }) Measure(specName, func(b Benchmarker) { - name := fmt.Sprintf("cluster-%s", util.RandomString(6)) + name := fmt.Sprintf("cluster-%s", namespace.Name) shared.SetEnvVar("USE_CI_ARTIFACTS", "true", false) kubernetesVersion := e2eCtx.E2EConfig.GetVariable(shared.KubernetesVersion) - // TODO(sbuerin): we always need ci artifacts, because we don't have images for every Kubernetes version - // * we're using ci-artifacts of the release we want and + flavor := shared.FlavorDefault // * with UseCIArtifacts we use the latest Kubernetes ci release - // if e2eCtx.Settings.UseCIArtifacts { - flavor := "conformance-ci-artifacts" if e2eCtx.Settings.UseCIArtifacts { var err error kubernetesVersion, err = kubernetesversions.LatestCIRelease() @@ -111,6 +107,6 @@ var _ = Describe("conformance tests", func() { AfterEach(func() { shared.SetEnvVar("USE_CI_ARTIFACTS", "false", false) // Dumps all the resources in the spec namespace, then cleanups the cluster object and the spec namespace itself. - shared.DumpSpecResourcesAndCleanup(ctx, "", namespace, e2eCtx) + shared.DumpSpecResourcesAndCleanup(ctx, specName, namespace, e2eCtx) }) }) diff --git a/test/e2e/suites/e2e/e2e_suite_test.go b/test/e2e/suites/e2e/e2e_suite_test.go new file mode 100644 index 0000000000..1b793d45ce --- /dev/null +++ b/test/e2e/suites/e2e/e2e_suite_test.go @@ -0,0 +1,53 @@ +// +build e2e + +/* +Copyright 2021 The Kubernetes Authors. + +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 ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/cluster-api/test/framework" + + "sigs.k8s.io/cluster-api-provider-openstack/test/e2e/shared" +) + +var e2eCtx *shared.E2EContext + +func init() { + e2eCtx = shared.NewE2EContext() + shared.CreateDefaultFlags(e2eCtx) +} + +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, "capo-e2e", []Reporter{framework.CreateJUnitReporterForProw(e2eCtx.Settings.ArtifactFolder)}) +} + +var _ = SynchronizedBeforeSuite(func() []byte { + return shared.Node1BeforeSuite(e2eCtx) +}, func(data []byte) { + shared.AllNodesBeforeSuite(e2eCtx, data) +}) + +var _ = SynchronizedAfterSuite(func() { + shared.AllNodesAfterSuite(e2eCtx) +}, func() { + shared.Node1AfterSuite(e2eCtx) +}) diff --git a/test/e2e/suites/e2e/e2e_test.go b/test/e2e/suites/e2e/e2e_test.go new file mode 100644 index 0000000000..8cf22dfffa --- /dev/null +++ b/test/e2e/suites/e2e/e2e_test.go @@ -0,0 +1,420 @@ +// +build e2e + +/* +Copyright 2021 The Kubernetes Authors. + +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" + "errors" + "fmt" + "path/filepath" + "strings" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apimachinerytypes "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/pointer" + clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" + bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1alpha4" + kubeadmv1beta1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/types/v1beta1" + "sigs.k8s.io/cluster-api/controllers/noderefutil" + "sigs.k8s.io/cluster-api/test/framework" + "sigs.k8s.io/cluster-api/test/framework/clusterctl" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-openstack/test/e2e/shared" +) + +var _ = Describe("e2e tests", func() { + var ( + namespace *corev1.Namespace + ctx context.Context + specName = "e2e" + ) + + BeforeEach(func() { + Expect(e2eCtx.Environment.BootstrapClusterProxy).ToNot(BeNil(), "Invalid argument. BootstrapClusterProxy can't be nil") + ctx = context.TODO() + // Setup a Namespace where to host objects for this spec and create a watcher for the namespace events. + namespace = shared.SetupSpecNamespace(ctx, specName, e2eCtx) + Expect(e2eCtx.E2EConfig).ToNot(BeNil(), "Invalid argument. e2eConfig can't be nil when calling %s spec", specName) + Expect(e2eCtx.E2EConfig.Variables).To(HaveKey(shared.KubernetesVersion)) + shared.SetEnvVar("USE_CI_ARTIFACTS", "true", false) + }) + + Describe("Workload cluster (default)", func() { + It("It should be creatable and deletable", func() { + shared.Byf("Creating a cluster") + clusterName := fmt.Sprintf("cluster-%s", namespace.Name) + configCluster := defaultConfigCluster(clusterName, namespace.Name) + configCluster.ControlPlaneMachineCount = pointer.Int64Ptr(3) + configCluster.WorkerMachineCount = pointer.Int64Ptr(1) + configCluster.Flavor = shared.FlavorDefault + md := createCluster(ctx, configCluster, specName) + + workerMachines := framework.GetMachinesByMachineDeployments(ctx, framework.GetMachinesByMachineDeploymentsInput{ + Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + ClusterName: clusterName, + Namespace: namespace.Name, + MachineDeployment: *md[0], + }) + controlPlaneMachines := framework.GetControlPlaneMachinesByCluster(ctx, framework.GetControlPlaneMachinesByClusterInput{ + Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + ClusterName: clusterName, + Namespace: namespace.Name, + }) + Expect(len(workerMachines)).To(Equal(1)) + Expect(len(controlPlaneMachines)).To(Equal(3)) + }) + }) + + Describe("Workload cluster (external cloud provider)", func() { + It("It should be creatable and deletable", func() { + shared.Byf("Creating a cluster") + clusterName := fmt.Sprintf("cluster-%s", namespace.Name) + configCluster := defaultConfigCluster(clusterName, namespace.Name) + configCluster.ControlPlaneMachineCount = pointer.Int64Ptr(1) + configCluster.WorkerMachineCount = pointer.Int64Ptr(1) + configCluster.Flavor = shared.FlavorExternalCloudProvider + md := createCluster(ctx, configCluster, specName) + + workerMachines := framework.GetMachinesByMachineDeployments(ctx, framework.GetMachinesByMachineDeploymentsInput{ + Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + ClusterName: clusterName, + Namespace: namespace.Name, + MachineDeployment: *md[0], + }) + controlPlaneMachines := framework.GetControlPlaneMachinesByCluster(ctx, framework.GetControlPlaneMachinesByClusterInput{ + Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + ClusterName: clusterName, + Namespace: namespace.Name, + }) + Expect(len(workerMachines)).To(Equal(1)) + Expect(len(controlPlaneMachines)).To(Equal(1)) + + shared.Byf("Waiting for worker nodes to be in Running phase") + statusChecks := []framework.MachineStatusCheck{framework.MachinePhaseCheck(string(clusterv1.MachinePhaseRunning))} + machineStatusInput := framework.WaitForMachineStatusCheckInput{ + Getter: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + Machine: &workerMachines[0], + StatusChecks: statusChecks, + } + framework.WaitForMachineStatusCheck(ctx, machineStatusInput, e2eCtx.E2EConfig.GetIntervals(specName, "wait-machine-status")...) + + workloadCluster := e2eCtx.Environment.BootstrapClusterProxy.GetWorkloadCluster(ctx, namespace.Name, clusterName) + + waitForDaemonSetRunning(ctx, workloadCluster.GetClient(), "kube-system", "openstack-cloud-controller-manager") + + waitForNodesReadyWithoutCCMTaint(ctx, workloadCluster.GetClient(), 2) + }) + }) + + Describe("Workload cluster (without lb)", func() { + It("It should be creatable and deletable", func() { + shared.Byf("Creating a cluster") + clusterName := fmt.Sprintf("cluster-%s", namespace.Name) + configCluster := defaultConfigCluster(clusterName, namespace.Name) + configCluster.ControlPlaneMachineCount = pointer.Int64Ptr(1) + configCluster.WorkerMachineCount = pointer.Int64Ptr(1) + configCluster.Flavor = shared.FlavorWithoutLB + md := createCluster(ctx, configCluster, specName) + + workerMachines := framework.GetMachinesByMachineDeployments(ctx, framework.GetMachinesByMachineDeploymentsInput{ + Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + ClusterName: clusterName, + Namespace: namespace.Name, + MachineDeployment: *md[0], + }) + controlPlaneMachines := framework.GetControlPlaneMachinesByCluster(ctx, framework.GetControlPlaneMachinesByClusterInput{ + Lister: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + ClusterName: clusterName, + Namespace: namespace.Name, + }) + Expect(len(workerMachines)).To(Equal(1)) + Expect(len(controlPlaneMachines)).To(Equal(1)) + }) + }) + + Describe("MachineDeployment misconfigurations", func() { + It("Should fail to create MachineDeployment with invalid subnet or invalid availability zone", func() { + shared.Byf("Creating a cluster") + clusterName := fmt.Sprintf("cluster-%s", namespace.Name) + configCluster := defaultConfigCluster(clusterName, namespace.Name) + configCluster.ControlPlaneMachineCount = pointer.Int64Ptr(1) + configCluster.WorkerMachineCount = pointer.Int64Ptr(0) + configCluster.Flavor = shared.FlavorDefault + _ = createCluster(ctx, configCluster, specName) + + shared.Byf("Creating Machine Deployment with invalid subnet ID") + md1Name := clusterName + "-md-1" + framework.CreateMachineDeployment(ctx, framework.CreateMachineDeploymentInput{ + Creator: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + MachineDeployment: makeMachineDeployment(namespace.Name, md1Name, clusterName, "", 1), + BootstrapConfigTemplate: makeJoinBootstrapConfigTemplate(namespace.Name, md1Name), + InfraMachineTemplate: makeOpenStackMachineTemplate(namespace.Name, clusterName, md1Name, "invalid-subnet"), + }) + + shared.Byf("Looking for failure event to be reported") + Eventually(func() bool { + eventList := getEvents(namespace.Name) + subnetError := "Failed to create server: no ports with fixed IPs found on Subnet \"invalid-subnet\"" + return isErrorEventExists(namespace.Name, md1Name, "FailedCreateServer", subnetError, eventList) + }, e2eCtx.E2EConfig.GetIntervals(specName, "wait-worker-nodes")...).Should(BeTrue()) + + shared.Byf("Creating Machine Deployment in an invalid Availability Zone") + md2Name := clusterName + "-md-2" + framework.CreateMachineDeployment(ctx, framework.CreateMachineDeploymentInput{ + Creator: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + MachineDeployment: makeMachineDeployment(namespace.Name, md2Name, clusterName, "invalid-az", 1), + BootstrapConfigTemplate: makeJoinBootstrapConfigTemplate(namespace.Name, md2Name), + InfraMachineTemplate: makeOpenStackMachineTemplate(namespace.Name, clusterName, md2Name, ""), + }) + + shared.Byf("Looking for failure event to be reported") + Eventually(func() bool { + eventList := getEvents(namespace.Name) + azError := "The requested availability zone is not available" + return isErrorEventExists(namespace.Name, md2Name, "FailedCreateServer", azError, eventList) + }, e2eCtx.E2EConfig.GetIntervals(specName, "wait-worker-nodes")...).Should(BeTrue()) + }) + }) + + AfterEach(func() { + shared.SetEnvVar("USE_CI_ARTIFACTS", "false", false) + // Dumps all the resources in the spec namespace, then cleanups the cluster object and the spec namespace itself. + shared.DumpSpecResourcesAndCleanup(ctx, specName, namespace, e2eCtx) + }) +}) + +func createCluster(ctx context.Context, configCluster clusterctl.ConfigClusterInput, specName string) []*clusterv1.MachineDeployment { + result := &clusterctl.ApplyClusterTemplateAndWaitResult{} + clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{ + ClusterProxy: e2eCtx.Environment.BootstrapClusterProxy, + ConfigCluster: configCluster, + WaitForClusterIntervals: e2eCtx.E2EConfig.GetIntervals(specName, "wait-cluster"), + WaitForControlPlaneIntervals: e2eCtx.E2EConfig.GetIntervals(specName, "wait-control-plane"), + WaitForMachineDeployments: e2eCtx.E2EConfig.GetIntervals(specName, "wait-worker-nodes"), + }, result) + + return result.MachineDeployments +} + +func defaultConfigCluster(clusterName, namespace string) clusterctl.ConfigClusterInput { + return clusterctl.ConfigClusterInput{ + LogFolder: filepath.Join(e2eCtx.Settings.ArtifactFolder, "clusters", e2eCtx.Environment.BootstrapClusterProxy.GetName()), + ClusterctlConfigPath: e2eCtx.Environment.ClusterctlConfigPath, + KubeconfigPath: e2eCtx.Environment.BootstrapClusterProxy.GetKubeconfigPath(), + InfrastructureProvider: clusterctl.DefaultInfrastructureProvider, + Namespace: namespace, + ClusterName: clusterName, + KubernetesVersion: e2eCtx.E2EConfig.GetVariable(shared.KubernetesVersion), + } +} + +func getEvents(namespace string) *corev1.EventList { + eventsList := &corev1.EventList{} + if err := e2eCtx.Environment.BootstrapClusterProxy.GetClient().List(context.TODO(), eventsList, crclient.InNamespace(namespace), crclient.MatchingLabels{}); err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Got error while fetching events of namespace: %s, %s \n", namespace, err.Error()) + } + + return eventsList +} + +func isErrorEventExists(namespace, machineDeploymentName, eventReason, errorMsg string, eList *corev1.EventList) bool { + ctrlClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient() + machineDeployment := &clusterv1.MachineDeployment{} + if err := ctrlClient.Get(context.TODO(), apimachinerytypes.NamespacedName{Namespace: namespace, Name: machineDeploymentName}, machineDeployment); err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Got error while getting machinedeployment %s \n", machineDeploymentName) + return false + } + + selector, err := metav1.LabelSelectorAsMap(&machineDeployment.Spec.Selector) + if err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Got error while reading lables of machinedeployment: %s, %s \n", machineDeploymentName, err.Error()) + return false + } + + openStackMachineList := &infrav1.OpenStackMachineList{} + if err := ctrlClient.List(context.TODO(), openStackMachineList, crclient.InNamespace(namespace), crclient.MatchingLabels(selector)); err != nil { + _, _ = fmt.Fprintf(GinkgoWriter, "Got error while getting openstackmachines of machinedeployment: %s, %s \n", machineDeploymentName, err.Error()) + return false + } + + eventMachinesCnt := 0 + for _, openStackMachine := range openStackMachineList.Items { + for _, event := range eList.Items { + if strings.Contains(event.Name, openStackMachine.Name) && event.Reason == eventReason && strings.Contains(event.Message, errorMsg) { + eventMachinesCnt++ + break + } + } + } + return len(openStackMachineList.Items) == eventMachinesCnt +} + +func makeOpenStackMachineTemplate(namespace, clusterName, name string, subnetID string) *infrav1.OpenStackMachineTemplate { + return &infrav1.OpenStackMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: infrav1.OpenStackMachineTemplateSpec{ + Template: infrav1.OpenStackMachineTemplateResource{ + Spec: infrav1.OpenStackMachineSpec{ + Flavor: e2eCtx.E2EConfig.GetVariable(shared.OpenStackNodeMachineFlavor), + Image: e2eCtx.E2EConfig.GetVariable(shared.OpenStackImageName), + SSHKeyName: shared.DefaultSSHKeyPairName, + CloudName: e2eCtx.E2EConfig.GetVariable(shared.OpenStackCloud), + CloudsSecret: &corev1.SecretReference{ + Name: fmt.Sprintf("%s-cloud-config", clusterName), + }, + Subnet: subnetID, + }, + }, + }, + } +} + +// makeJoinBootstrapConfigTemplate returns a KubeadmConfigTemplate which can be used +// to test different error cases. As we're missing e.g. the cloud provider conf it cannot +// be used to successfully add nodes to a cluster. +func makeJoinBootstrapConfigTemplate(namespace, name string) *bootstrapv1.KubeadmConfigTemplate { + return &bootstrapv1.KubeadmConfigTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: bootstrapv1.KubeadmConfigTemplateSpec{ + Template: bootstrapv1.KubeadmConfigTemplateResource{ + Spec: bootstrapv1.KubeadmConfigSpec{ + JoinConfiguration: &kubeadmv1beta1.JoinConfiguration{ + NodeRegistration: kubeadmv1beta1.NodeRegistrationOptions{ + Name: "{{ local_hostname }}", + KubeletExtraArgs: map[string]string{ + "cloud-config": "/etc/kubernetes/cloud.conf", + "cloud-provider": "openstack", + }, + }, + }, + }, + }, + }, + } +} + +func makeMachineDeployment(namespace, mdName, clusterName string, failureDomain string, replicas int32) *clusterv1.MachineDeployment { + if failureDomain == "" { + failureDomain = e2eCtx.E2EConfig.GetVariable(shared.OpenStackFailureDomain) + } + return &clusterv1.MachineDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: mdName, + Namespace: namespace, + Labels: map[string]string{ + "cluster.x-k8s.io/cluster-name": clusterName, + "nodepool": mdName, + }, + }, + Spec: clusterv1.MachineDeploymentSpec{ + Replicas: &replicas, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "cluster.x-k8s.io/cluster-name": clusterName, + "nodepool": mdName, + }, + }, + ClusterName: clusterName, + Template: clusterv1.MachineTemplateSpec{ + ObjectMeta: clusterv1.ObjectMeta{ + Labels: map[string]string{ + "cluster.x-k8s.io/cluster-name": clusterName, + "nodepool": mdName, + }, + }, + Spec: clusterv1.MachineSpec{ + ClusterName: clusterName, + FailureDomain: &failureDomain, + Bootstrap: clusterv1.Bootstrap{ + ConfigRef: &corev1.ObjectReference{ + Kind: "KubeadmConfigTemplate", + APIVersion: bootstrapv1.GroupVersion.String(), + Name: mdName, + Namespace: namespace, + }, + }, + InfrastructureRef: corev1.ObjectReference{ + Kind: "OpenStackMachineTemplate", + APIVersion: infrav1.GroupVersion.String(), + Name: mdName, + Namespace: namespace, + }, + Version: pointer.StringPtr(e2eCtx.E2EConfig.GetVariable(shared.KubernetesVersion)), + }, + }, + }, + } +} + +func waitForDaemonSetRunning(ctx context.Context, ctrlClient crclient.Client, namespace, name string) { + shared.Byf("Ensuring DaemonSet %s is running", name) + daemonSet := &appsv1.DaemonSet{} + Eventually( + func() (bool, error) { + if err := ctrlClient.Get(ctx, apimachinerytypes.NamespacedName{Namespace: namespace, Name: name}, daemonSet); err != nil { + return false, err + } + return daemonSet.Status.CurrentNumberScheduled == daemonSet.Status.NumberReady, nil + }, 10*time.Minute, 30*time.Second, + ).Should(BeTrue()) +} + +func waitForNodesReadyWithoutCCMTaint(ctx context.Context, ctrlClient crclient.Client, nodeCount int) { + shared.Byf("Waiting for the workload nodes to be ready") + Eventually(func() (int, error) { + nodeList := &corev1.NodeList{} + if err := ctrlClient.List(ctx, nodeList); err != nil { + return 0, err + } + if len(nodeList.Items) == 0 { + return 0, errors.New("no nodes were found") + } + + count := 0 + for _, node := range nodeList.Items { + n := node + if noderefutil.IsNodeReady(&n) && isCloudProviderInitialized(node.Spec.Taints) { + count++ + } + } + return count, nil + }, "10m", "10s").Should(Equal(nodeCount)) +} + +func isCloudProviderInitialized(taints []corev1.Taint) bool { + for _, taint := range taints { + if taint.Key == "node.cloudprovider.kubernetes.io/uninitialized" { + return false + } + } + return true +}