Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[release-v1.58] Check content-type in importer to warn against unexpected kubevirt imports #2975

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions pkg/importer/http-datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ import (
)

const (
tempFile = "tmpimage"
nbdkitPid = "/tmp/nbdkit.pid"
nbdkitSocket = "/tmp/nbdkit.sock"
defaultUserAgent = "cdi-golang-importer"
tempFile = "tmpimage"
nbdkitPid = "/tmp/nbdkit.pid"
nbdkitSocket = "/tmp/nbdkit.sock"
defaultUserAgent = "cdi-golang-importer"
httpContentType = "Content-Type"
httpContentLength = "Content-Length"
)

// HTTPDataSource is the data provider for http(s) endpoints.
Expand Down Expand Up @@ -96,7 +98,7 @@ func NewHTTPDataSource(endpoint, accessKey, secKey, certDir string, contentType
return nil, errors.Wrap(err, "Error getting extra headers for HTTP client")
}

httpReader, contentLength, brokenForQemuImg, err := createHTTPReader(ctx, ep, accessKey, secKey, certDir, extraHeaders, secretExtraHeaders)
httpReader, contentLength, brokenForQemuImg, err := createHTTPReader(ctx, ep, accessKey, secKey, certDir, extraHeaders, secretExtraHeaders, contentType)
if err != nil {
cancel()
return nil, err
Expand Down Expand Up @@ -294,7 +296,7 @@ func addExtraheaders(req *http.Request, extraHeaders []string) {
req.Header.Add("User-Agent", defaultUserAgent)
}

func createHTTPReader(ctx context.Context, ep *url.URL, accessKey, secKey, certDir string, extraHeaders, secretExtraHeaders []string) (io.ReadCloser, uint64, bool, error) {
func createHTTPReader(ctx context.Context, ep *url.URL, accessKey, secKey, certDir string, extraHeaders, secretExtraHeaders []string, contentType cdiv1.DataVolumeContentType) (io.ReadCloser, uint64, bool, error) {
var brokenForQemuImg bool
client, err := createHTTPClient(certDir)
if err != nil {
Expand Down Expand Up @@ -334,6 +336,16 @@ func createHTTPReader(ctx context.Context, ep *url.URL, accessKey, secKey, certD
return nil, uint64(0), true, errors.Errorf("expected status code 200, got %d. Status: %s", resp.StatusCode, resp.Status)
}

if contentType == cdiv1.DataVolumeKubeVirt {
// Check the content-type if we are expecting a KubeVirt img.
if val, ok := resp.Header[httpContentType]; ok {
if strings.HasPrefix(val[0], "text/") {
// We will continue with the import nonetheless, but content might be unexpected.
klog.Warningf("Unexpected content type '%s'. Content might not be a KubeVirt image.", val[0])
}
}
}

acceptRanges, ok := resp.Header["Accept-Ranges"]
if !ok || acceptRanges[0] == "none" {
klog.V(2).Infof("Accept-Ranges isn't bytes, avoiding qemu-img")
Expand Down Expand Up @@ -416,7 +428,7 @@ func getContentLength(client *http.Client, ep *url.URL, accessKey, secKey string
func parseHTTPHeader(resp *http.Response) uint64 {
var err error
total := uint64(0)
if val, ok := resp.Header["Content-Length"]; ok {
if val, ok := resp.Header[httpContentLength]; ok {
total, err = strconv.ParseUint(val[0], 10, 64)
if err != nil {
klog.Errorf("could not convert content length, got %v", err)
Expand Down
18 changes: 9 additions & 9 deletions pkg/importer/http-datasource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ var _ = Describe("Http client", func() {

var _ = Describe("Http reader", func() {
It("should fail when passed an invalid cert directory", func() {
_, total, _, err := createHTTPReader(context.Background(), nil, "", "", "/invalid", nil, nil)
_, total, _, err := createHTTPReader(context.Background(), nil, "", "", "/invalid", nil, nil, cdiv1.DataVolumeKubeVirt)
Expect(err).To(HaveOccurred())
Expect(uint64(0)).To(Equal(total))
})
Expand All @@ -249,7 +249,7 @@ var _ = Describe("Http reader", func() {
defer ts.Close()
ep, err := url.Parse(ts.URL)
Expect(err).ToNot(HaveOccurred())
r, total, _, err := createHTTPReader(context.Background(), ep, "user", "password", "", nil, nil)
r, total, _, err := createHTTPReader(context.Background(), ep, "user", "password", "", nil, nil, cdiv1.DataVolumeKubeVirt)
Expect(err).ToNot(HaveOccurred())
Expect(uint64(25)).To(Equal(total))
err = r.Close()
Expand All @@ -272,7 +272,7 @@ var _ = Describe("Http reader", func() {
defer ts.Close()
ep, err := url.Parse(ts.URL)
Expect(err).ToNot(HaveOccurred())
r, total, _, err := createHTTPReader(context.Background(), ep, "user", "password", "", nil, nil)
r, total, _, err := createHTTPReader(context.Background(), ep, "user", "password", "", nil, nil, cdiv1.DataVolumeKubeVirt)
Expect(err).ToNot(HaveOccurred())
Expect(uint64(25)).To(Equal(total))
err = r.Close()
Expand All @@ -294,7 +294,7 @@ var _ = Describe("Http reader", func() {
defer ts.Close()
ep, err := url.Parse(ts.URL)
Expect(err).ToNot(HaveOccurred())
r, total, brokenForQemuImg, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil)
r, total, brokenForQemuImg, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil, cdiv1.DataVolumeKubeVirt)
Expect(brokenForQemuImg).To(BeFalse())
Expect(err).ToNot(HaveOccurred())
Expect(uint64(25)).To(Equal(total))
Expand All @@ -315,7 +315,7 @@ var _ = Describe("Http reader", func() {
defer ts.Close()
ep, err := url.Parse(ts.URL)
Expect(err).ToNot(HaveOccurred())
r, total, _, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil)
r, total, _, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil, cdiv1.DataVolumeKubeVirt)
Expect(err).ToNot(HaveOccurred())
Expect(uint64(0)).To(Equal(total))
err = r.Close()
Expand All @@ -339,7 +339,7 @@ var _ = Describe("Http reader", func() {
defer ts.Close()
ep, err := url.Parse(ts.URL)
Expect(err).ToNot(HaveOccurred())
r, total, brokenForQemuImg, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil)
r, total, brokenForQemuImg, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil, cdiv1.DataVolumeKubeVirt)
Expect(brokenForQemuImg).To(BeTrue())
Expect(err).ToNot(HaveOccurred())
Expect(uint64(25)).To(Equal(total))
Expand All @@ -359,7 +359,7 @@ var _ = Describe("Http reader", func() {
defer ts.Close()
ep, err := url.Parse(ts.URL)
Expect(err).ToNot(HaveOccurred())
r, total, brokenForQemuImg, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil)
r, total, brokenForQemuImg, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil, cdiv1.DataVolumeKubeVirt)
Expect(brokenForQemuImg).To(BeTrue())
Expect(err).ToNot(HaveOccurred())
Expect(uint64(25)).To(Equal(total))
Expand All @@ -374,7 +374,7 @@ var _ = Describe("Http reader", func() {
defer ts.Close()
ep, err := url.Parse(ts.URL)
Expect(err).ToNot(HaveOccurred())
_, total, _, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil)
_, total, _, err := createHTTPReader(context.Background(), ep, "", "", "", nil, nil, cdiv1.DataVolumeKubeVirt)
Expect(err).To(HaveOccurred())
Expect(uint64(0)).To(Equal(total))
Expect("expected status code 200, got 500. Status: 500 Internal Server Error").To(Equal(err.Error()))
Expand All @@ -391,7 +391,7 @@ var _ = Describe("Http reader", func() {
defer ts.Close()
ep, err := url.Parse(ts.URL)
Expect(err).ToNot(HaveOccurred())
r, total, _, err := createHTTPReader(context.Background(), ep, "", "", "", []string{"Extra-Header: 123"}, nil)
r, total, _, err := createHTTPReader(context.Background(), ep, "", "", "", []string{"Extra-Header: 123"}, nil, cdiv1.DataVolumeKubeVirt)
Expect(err).ToNot(HaveOccurred())
Expect(uint64(0)).To(Equal(total))
err = r.Close()
Expand Down
30 changes: 30 additions & 0 deletions tests/badserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ package tests

import (
"fmt"
"strings"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"kubevirt.io/containerized-data-importer/pkg/common"
controller "kubevirt.io/containerized-data-importer/pkg/controller/common"
"kubevirt.io/containerized-data-importer/tests/framework"
"kubevirt.io/containerized-data-importer/tests/utils"

Expand All @@ -31,6 +36,31 @@ var _ = Describe("Problematic server responses", func() {
Entry("[rfe_id:4326][test_id:5076][crit:low][vendor:cnv-qe@redhat.com][level:component] Should succeed even if Accept-Ranges doesn't exist", "/no-accept-ranges/cirros-qcow2.img"),
)

It("Should warn against unexpected content-types when importing a kubevirt img", func() {
cdiBadServer := fmt.Sprintf("http://cdi-bad-webserver.%s:9090", f.CdiInstallNs)
pathname := "/bad-content-type/cirros-qcow2.img"
dataVolume = utils.NewDataVolumeWithHTTPImport("badserver-dv", "1Gi", cdiBadServer+pathname)
dataVolume.Annotations[controller.AnnPodRetainAfterCompletion] = "true"
By("creating DataVolume")
dataVolume, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolume)
Expect(err).ToNot(HaveOccurred())
f.ForceBindPvcIfDvIsWaitForFirstConsumer(dataVolume)

err = utils.WaitForDataVolumePhase(f, f.Namespace.Name, cdiv1.Succeeded, dataVolume.Name)
Expect(err).ToNot(HaveOccurred())

By("Find importer pod after completion")
importer, err := utils.FindPodByPrefixOnce(f.K8sClient, dataVolume.Namespace, common.ImporterPodName, common.CDILabelSelector)
Expect(err).ToNot(HaveOccurred())
Expect(importer.DeletionTimestamp).To(BeNil())

By("Verify HTTP request error in importer log")
Eventually(func() bool {
log, _ := f.RunKubectlCommand("logs", importer.Name, "-n", importer.Namespace)
return strings.Contains(log, "Unexpected content type 'text/html'. Content might not be a KubeVirt image.")
}, time.Minute, pollingInterval).Should(BeTrue())
})

AfterEach(func() {
By("deleting DataVolume")
err := utils.DeleteDataVolume(f.CdiClient, f.Namespace.Name, dataVolumeName)
Expand Down
17 changes: 17 additions & 0 deletions tools/cdi-func-test-bad-webserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ func flaky(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}

func badContentType(w http.ResponseWriter, r *http.Request) {
actualFileURL := getEquivalentFileHostURL(r.URL.String())

resp, err := http.Get(actualFileURL)
if err != nil {
panic("Couldn't fetch URL")
}

w.Header().Set("Content-Type", "text/html")

_, err = io.Copy(w, resp.Body)
if err != nil {
panic(fmt.Errorf("badContentType: failed to write the response; %w", err))
}
}

func noAcceptRanges(w http.ResponseWriter, r *http.Request) {
actualFileURL := getEquivalentFileHostURL(r.URL.String())

Expand Down Expand Up @@ -79,6 +95,7 @@ func main() {
http.HandleFunc("/forbidden-HEAD/", failHEAD)
http.HandleFunc("/flaky/", flaky)
http.HandleFunc("/no-accept-ranges/", noAcceptRanges)
http.HandleFunc("/bad-content-type/", badContentType)
err := http.ListenAndServe(":9090", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
Expand Down