Skip to content

Commit

Permalink
Load images in e2e tests with openstackimage controller
Browse files Browse the repository at this point in the history
  • Loading branch information
mdbooth committed Jul 18, 2024
1 parent 24c966c commit d68dda6
Show file tree
Hide file tree
Showing 12 changed files with 396 additions and 37 deletions.
16 changes: 4 additions & 12 deletions hack/ci/cloud-init/controller.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,13 @@
ENABLED_SERVICES+=${OPENSTACK_ADDITIONAL_SERVICES}
DISABLED_SERVICES+=${OPENSTACK_DISABLED_SERVICES}

# Don't download default images, just our test images
# Don't download default images.
# Test images are downloaded by the e2e tests directly.
DOWNLOAD_DEFAULT_IMAGES=False
# We upload the Amphora image so it doesn't have to be built
IMAGE_URLS="https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/amphora/2022-12-05/amphora-x64-haproxy.qcow2"
# Increase the total image size limit
GLANCE_LIMIT_IMAGE_SIZE_TOTAL=20000
# We upload the Amphora image so it doesn't have to be build
# Upload the images so we don't have to upload them from Prow
# NOTE: If you get issues when changing/adding images, check if the limits
# are sufficient and change the variable above if needed.
# https://docs.openstack.org/glance/latest/admin/quotas.html
IMAGE_URLS="https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/amphora/2022-12-05/amphora-x64-haproxy.qcow2,"
IMAGE_URLS+="https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/cirros/2022-12-05/cirros-0.6.1-x86_64-disk.img,"
IMAGE_URLS+="https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/ubuntu/2024-05-28/ubuntu-2204-kube-v1.29.5.img,"
IMAGE_URLS+="https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/ubuntu/2024-05-28/ubuntu-2204-kube-v1.30.1.img,"
IMAGE_URLS+="https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/flatcar/flatcar-stable-3815.2.2-kube-v1.30.1.img,"
IMAGE_URLS+="https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_openstack_image.img"

[[post-config|$NOVA_CONF]]
[DEFAULT]
Expand Down
10 changes: 5 additions & 5 deletions pkg/scope/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ import (
)

const (
cloudsSecretKey = "clouds.yaml"
caSecretKey = "cacert"
CloudsSecretKey = "clouds.yaml"
CASecretKey = "cacert"
)

type providerScopeFactory struct {
Expand Down Expand Up @@ -267,18 +267,18 @@ func getCloudFromSecret(ctx context.Context, ctrlClient client.Client, secretNam
return emptyCloud, nil, err
}

content, ok := secret.Data[cloudsSecretKey]
content, ok := secret.Data[CloudsSecretKey]
if !ok {
return emptyCloud, nil, fmt.Errorf("OpenStack credentials secret %v did not contain key %v",
secretName, cloudsSecretKey)
secretName, CloudsSecretKey)
}
var clouds clientconfig.Clouds
if err = yaml.Unmarshal(content, &clouds); err != nil {
return emptyCloud, nil, fmt.Errorf("failed to unmarshal clouds credentials stored in secret %v: %v", secretName, err)
}

// get caCert
caCert, ok := secret.Data[caSecretKey]
caCert, ok := secret.Data[CASecretKey]
if !ok {
return clouds.Clouds[cloudName], nil, nil
}
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/data/e2e_conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,5 @@ intervals:
default/wait-machine-upgrade: ["30m", "10s"]
default/wait-nodes-ready: ["15m", "10s"]
default/wait-machine-remediation: ["10m", "10s"]
default/wait-image-create: ["15m", "10s"]
default/wait-image-delete: ["2m", "10s"]
14 changes: 12 additions & 2 deletions test/e2e/shared/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ limitations under the License.
package shared

import (
"errors"
"flag"

"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/cluster-api/test/framework"

infrav1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1"
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
)

Expand All @@ -38,6 +40,7 @@ const (
OpenStackBastionFlavorAlt = "OPENSTACK_BASTION_MACHINE_FLAVOR_ALT"
OpenStackCloudYAMLFile = "OPENSTACK_CLOUD_YAML_FILE"
OpenStackCloud = "OPENSTACK_CLOUD"
OpenStackCloudCACertB64 = "OPENSTACK_CLOUD_CACERT_B64"
OpenStackCloudAdmin = "OPENSTACK_CLOUD_ADMIN"
OpenStackFailureDomain = "OPENSTACK_FAILURE_DOMAIN" //nolint:gosec // Linter thinks this could be credentials...
OpenStackFailureDomainAlt = "OPENSTACK_FAILURE_DOMAIN_ALT"
Expand All @@ -62,8 +65,15 @@ const (
func DefaultScheme() *runtime.Scheme {
sc := runtime.NewScheme()
framework.TryAddDefaultSchemes(sc)
_ = infrav1.AddToScheme(sc)
_ = clientgoscheme.AddToScheme(sc)

err := errors.Join(
infrav1alpha1.AddToScheme(sc),
infrav1.AddToScheme(sc),
clientgoscheme.AddToScheme(sc),
)
if err != nil {
panic("error adding types to scheme: " + err.Error())
}
return sc
}

Expand Down
272 changes: 272 additions & 0 deletions test/e2e/shared/images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
//go:build e2e
// +build e2e

/*
Copyright 2024 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 shared

import (
"context"
"encoding/base64"
neturl "net/url"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
meta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

infrav1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1"
applyconfigv1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/pkg/generated/applyconfiguration/api/v1alpha1"
applyconfiginfrav1 "sigs.k8s.io/cluster-api-provider-openstack/pkg/generated/applyconfiguration/api/v1beta1"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
"sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/ssa"
)

const (
// The base URL of the CAPO staging artifacts bucket.
stagingArtifactBase = "https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/"

// The name of the credentials secret.
credentialsSecretName = "openstack-credentials" //nolint:gosec // these aren't hard-coded credentials

// A tag which will be added to all glances images created by these tests.
E2EImageTag = "capo-e2e"
)

type DownloadImage struct {
// The name of the OpenStackImage object
Name string
// An absolute URL to download the image from
URL string
// A path relative to the staging artifact repo to download the image from
ArtifactPath string
// A hash used to verify the downloaded image
Hash, HashAlgorithm string
}

func CoreImages(e2eCtx *E2EContext) []DownloadImage {
return []DownloadImage{
{
Name: "cirros",
ArtifactPath: "cirros/2022-12-05/" + e2eCtx.E2EConfig.GetVariable("OPENSTACK_BASTION_IMAGE_NAME") + ".img",

// Specifying an image hash means we can't use
// web-download. This serves as an E2E test of image
// upload. We only do this with cirros because it's very
// small and it's going to involve data transfer between
// the test runner and wherever OpenStack is running
HashAlgorithm: string(infrav1alpha1.ImageHashAlgorithmMD5),
// From: https://download.cirros-cloud.net/0.6.1/MD5SUMS
Hash: "0c839612eb3f2469420f2ccae990827f",
},
{
Name: "capo-default",
ArtifactPath: "ubuntu/2024-05-28/" + e2eCtx.E2EConfig.GetVariable("OPENSTACK_IMAGE_NAME") + ".img",
},
}
}

func CreateGlanceCredentials(ctx context.Context, e2eCtx *E2EContext) {
k8sClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()

// Generate the credentials secret each image will reference
credentialsSecret := generateCredentialsSecret(e2eCtx)
Expect(k8sClient.Create(ctx, credentialsSecret)).To(Succeed(), "create openstack-credentials in default namespace")
}

func imageNames(images []DownloadImage) string {
names := make([]string, len(images))
for i := range images {
names[i] = images[i].Name
}
return strings.Join(names, ", ")
}

func EnsureGlanceImages(ctx context.Context, e2eCtx *E2EContext, images []DownloadImage) {
By("Ensuring glances images have been created: " + imageNames(images))

k8sClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()

for _, image := range images {
// Infer the url if none was specified
url := image.URL
if url == "" {
url = stagingArtifactBase + image.ArtifactPath
}

// Infer the name to use for the glance image

// Use last part of url
u, err := neturl.Parse(url)
Expect(err).NotTo(HaveOccurred(), "parsing "+url)
d := strings.Split(u.Path, "/")
Expect(len(d)).To(BeNumerically(">", 1), "Not enough path elements in "+url)
glanceName := d[len(d)-1]

// Remove the type suffix
for _, suffix := range []string{".img", ".qcow2"} {
if strings.HasSuffix(glanceName, suffix) {
glanceName = glanceName[:len(glanceName)-len(suffix)]
continue
}
}

var imageHash *infrav1alpha1.ImageHash
if image.HashAlgorithm != "" && image.Hash != "" {
imageHash = &infrav1alpha1.ImageHash{
Algorithm: infrav1alpha1.ImageHashAlgorithm(image.HashAlgorithm),
Value: image.Hash,
}
}

// Generate and create the image
openStackImage, applyConfig := generateOpenStackImage(e2eCtx, image.Name, glanceName, url, imageHash)
Logf("Ensuring glance image " + image.Name)
Expect(k8sClient.Patch(ctx, openStackImage, ssa.ApplyConfigPatch(applyConfig), client.ForceOwnership, client.FieldOwner("capo-e2e"))).To(Succeed(), "ensure image "+image.Name)
}
}

func WaitForGlanceImagesAvailable(ctx context.Context, e2eCtx *E2EContext, images []DownloadImage) {
names := imageNames(images)
By("Waiting for glance images to become available: " + names)

k8sClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()

Eventually(func(ctx context.Context) []string {
By("Polling images")

openStackImageList := &infrav1alpha1.OpenStackImageList{}
Expect(k8sClient.List(ctx, openStackImageList, client.InNamespace("default"))).To(Succeed())

var available []string
var notAvailable []string
for i := range images {
imageName := images[i].Name
image := func() *infrav1alpha1.OpenStackImage {
for j := range openStackImageList.Items {
item := &openStackImageList.Items[j]
if item.Name == imageName {
return item
}
}
return nil
}()

Expect(image).ToNot(BeNil(), "Did not find "+imageName+" in image list")

availableCondition := meta.FindStatusCondition(image.Status.Conditions, infrav1alpha1.OpenStackConditionAvailable)
if availableCondition == nil || availableCondition.Status != metav1.ConditionTrue {
var msg string
if availableCondition == nil {
msg = "no status yet"
} else {
msg = availableCondition.Message
}
notAvailable = append(notAvailable, image.Name+": "+msg)
} else {
available = append(available, image.Name)
}
}
Logf("Available: " + strings.Join(available, ", "))
Logf("Not available: " + strings.Join(notAvailable, ", "))

return notAvailable
}, e2eCtx.E2EConfig.GetIntervals("default", "wait-image-create")...).WithContext(ctx).Should(BeEmpty(), "OpenStackImages are not available")

Logf("Glance images became available: " + names)
}

func DeleteAllOpenStackImages(ctx context.Context, e2eCtx *E2EContext) {
k8sClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()

By("Deleting glance images")
Eventually(func() []infrav1alpha1.OpenStackImage {
By("Fetching remaining images")

openStackImageList := &infrav1alpha1.OpenStackImageList{}
Expect(k8sClient.List(ctx, openStackImageList, client.InNamespace("default"))).To(Succeed())
images := openStackImageList.Items

for i := range images {
image := &images[i]
if image.GetDeletionTimestamp().IsZero() {
Expect(k8sClient.Delete(ctx, image)).To(Succeed())
Logf("Deleted OpenStackImage " + image.Name)
} else {
Logf("OpenStackImage " + image.Name + " is still deleting")
}
}

return images
}, e2eCtx.E2EConfig.GetIntervals("default", "wait-image-delete")...).Should(BeEmpty(), "OpenStackImages were not deleted")
}

func generateCredentialsSecret(e2eCtx *E2EContext) *corev1.Secret {
// We run in Node1BeforeSuite, so OPENSTACK_CLOUD_YAML_B64 is not yet set
openStackCloudYAMLFile := e2eCtx.E2EConfig.GetVariable(OpenStackCloudYAMLFile)
caCertB64 := e2eCtx.E2EConfig.GetVariable(OpenStackCloudCACertB64)
caCert, err := base64.StdEncoding.DecodeString(caCertB64)
Expect(err).NotTo(HaveOccurred(), "base64 decode CA Cert: "+caCertB64)

cloudsYAML := getOpenStackCloudYAML(openStackCloudYAMLFile)
credentialsSecret := corev1.Secret{
StringData: map[string]string{
scope.CloudsSecretKey: string(cloudsYAML),
scope.CASecretKey: string(caCert),
},
}
credentialsSecret.SetName(credentialsSecretName)
credentialsSecret.SetNamespace("default")

return &credentialsSecret
}

func generateOpenStackImage(e2eCtx *E2EContext, name, glanceName, url string, downloadHash *infrav1alpha1.ImageHash) (*infrav1alpha1.OpenStackImage, *applyconfigv1alpha1.OpenStackImageApplyConfiguration) {
const imageNamespace = "default"

applyConfig := applyconfigv1alpha1.OpenStackImage(name, imageNamespace).
WithSpec(applyconfigv1alpha1.OpenStackImageSpec().
WithImageName(glanceName).
WithContainerFormat(infrav1alpha1.ImageContainerFormatBare).
WithDiskFormat(infrav1alpha1.ImageDiskFormatQCOW2).
WithTags(E2EImageTag).
WithIdentityRef(applyconfiginfrav1.OpenStackIdentityReference().
WithName(credentialsSecretName).
WithCloudName(e2eCtx.E2EConfig.GetVariable("OPENSTACK_CLOUD"))).
WithContent(applyconfigv1alpha1.ImageContent().
WithSource(applyconfigv1alpha1.ImageSource().
WithType(infrav1alpha1.ImageSourceTypeURL).
WithURL(applyconfigv1alpha1.ImageSourceURL().
WithURL(url)))))

if downloadHash != nil {
applyConfig.Spec.Content.
WithDownloadHash(applyconfigv1alpha1.ImageHash().
WithAlgorithm(downloadHash.Algorithm).
WithValue(downloadHash.Value)).
WithImageHashAlgorithm(infrav1alpha1.ImageHashAlgorithmSHA512)
}

openStackImage := &infrav1alpha1.OpenStackImage{}
openStackImage.Name = name
openStackImage.Namespace = imageNamespace

return openStackImage, applyConfig
}
Loading

0 comments on commit d68dda6

Please sign in to comment.