github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/environs/imagedownloads/simplestreams_test.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package imagedownloads_test
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"strings"
    12  
    13  	"github.com/juju/testing"
    14  	jc "github.com/juju/testing/checkers"
    15  	"golang.org/x/crypto/openpgp"
    16  	openpgperrors "golang.org/x/crypto/openpgp/errors"
    17  	gc "gopkg.in/check.v1"
    18  
    19  	corebase "github.com/juju/juju/core/base"
    20  	. "github.com/juju/juju/environs/imagedownloads"
    21  	"github.com/juju/juju/environs/imagemetadata"
    22  	"github.com/juju/juju/environs/simplestreams"
    23  	streamstesting "github.com/juju/juju/environs/simplestreams/testing"
    24  )
    25  
    26  type Suite struct {
    27  	testing.IsolationSuite
    28  }
    29  
    30  var _ = gc.Suite(&Suite{})
    31  
    32  func newTestDataSource(factory simplestreams.DataSourceFactory, s string) simplestreams.DataSource {
    33  	return NewDataSource(factory, s+"/"+imagemetadata.ReleasedImagesPath)
    34  }
    35  
    36  func newTestDataSourceFunc(s string) func() simplestreams.DataSource {
    37  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
    38  	return func() simplestreams.DataSource {
    39  		return NewDataSource(ss, s+"/releases/")
    40  	}
    41  }
    42  
    43  func (s *Suite) SetUpTest(c *gc.C) {
    44  	s.PatchValue(&corebase.UbuntuDistroInfo, "/path/notexists")
    45  	imagemetadata.SimplestreamsImagesPublicKey = streamstesting.SignedMetadataPublicKey
    46  
    47  	// The index.sjson file used by these tests have been regenerated using
    48  	// the test keys in environs/simplestreams/testing/testing.go. As this
    49  	// signature is not trusted, we need to override the signature check
    50  	// implementation and suppress the ErrUnkownIssuer error.
    51  	s.PatchValue(&simplestreams.PGPSignatureCheckFn, func(keyring openpgp.KeyRing, signed, signature io.Reader) (*openpgp.Entity, error) {
    52  		ent, err := openpgp.CheckDetachedSignature(keyring, signed, signature)
    53  		c.Assert(err, gc.Equals, openpgperrors.ErrUnknownIssuer, gc.Commentf("expected the signature verification to return ErrUnknownIssuer when the index file is signed with the test pgp key"))
    54  		return ent, nil
    55  	})
    56  }
    57  
    58  func (*Suite) TestNewSignedImagesSource(c *gc.C) {
    59  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
    60  	got := DefaultSource(ss)()
    61  	c.Check(got.Description(), jc.DeepEquals, "ubuntu cloud images")
    62  	c.Check(got.PublicSigningKey(), jc.DeepEquals, imagemetadata.SimplestreamsImagesPublicKey)
    63  	c.Check(got.RequireSigned(), jc.IsTrue)
    64  	gotURL, err := got.URL("")
    65  	c.Assert(err, jc.ErrorIsNil)
    66  	c.Assert(gotURL, jc.DeepEquals, "http://cloud-images.ubuntu.com/releases/")
    67  }
    68  
    69  func (*Suite) TestFetchManyDefaultFilter(c *gc.C) {
    70  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
    71  	ts := httptest.NewServer(&sstreamsHandler{})
    72  	defer ts.Close()
    73  	tds := []simplestreams.DataSource{
    74  		newTestDataSource(ss, ts.URL)}
    75  	constraints, err := imagemetadata.NewImageConstraint(
    76  		simplestreams.LookupParams{
    77  			Arches:   []string{"amd64", "arm64", "ppc64el"},
    78  			Releases: []string{"16.04"},
    79  			Stream:   "released",
    80  		},
    81  	)
    82  	c.Assert(err, jc.ErrorIsNil)
    83  	got, resolveInfo, err := Fetch(ss, tds, constraints, nil)
    84  	c.Check(resolveInfo.Signed, jc.IsTrue)
    85  	c.Check(err, jc.ErrorIsNil)
    86  	c.Assert(len(got), jc.DeepEquals, 27)
    87  	for _, v := range got {
    88  		gotURL, err := v.DownloadURL(ts.URL)
    89  		c.Check(err, jc.ErrorIsNil)
    90  		c.Check(strings.HasSuffix(gotURL.String(), v.FType), jc.IsTrue)
    91  		c.Check(strings.Contains(gotURL.String(), v.Release), jc.IsTrue)
    92  		c.Check(strings.Contains(gotURL.String(), v.Version), jc.IsTrue)
    93  	}
    94  }
    95  
    96  func (*Suite) TestFetchManyDefaultFilterAndCustomImageDownloadURL(c *gc.C) {
    97  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
    98  	ts := httptest.NewServer(&sstreamsHandler{})
    99  	defer ts.Close()
   100  	tds := []simplestreams.DataSource{
   101  		newTestDataSource(ss, ts.URL)}
   102  	constraints, err := imagemetadata.NewImageConstraint(
   103  		simplestreams.LookupParams{
   104  			Arches:   []string{"amd64", "arm64", "ppc64el"},
   105  			Releases: []string{"16.04"},
   106  			Stream:   "released",
   107  		},
   108  	)
   109  	c.Assert(err, jc.ErrorIsNil)
   110  	got, resolveInfo, err := Fetch(ss, tds, constraints, nil)
   111  	c.Check(resolveInfo.Signed, jc.IsTrue)
   112  	c.Check(err, jc.ErrorIsNil)
   113  	c.Assert(len(got), jc.DeepEquals, 27)
   114  	for _, v := range got {
   115  		// Note: instead of the index URL, we are pulling the actual
   116  		// images from a different operator-provided URL.
   117  		gotURL, err := v.DownloadURL("https://tasty-cloud-images.ubuntu.com")
   118  		c.Check(err, jc.ErrorIsNil)
   119  		c.Check(strings.HasPrefix(gotURL.String(), "https://tasty-cloud-images.ubuntu.com"), jc.IsTrue, gc.Commentf("expected image download URL to use the operator-provided URL"))
   120  		c.Check(strings.HasSuffix(gotURL.String(), v.FType), jc.IsTrue)
   121  		c.Check(strings.Contains(gotURL.String(), v.Release), jc.IsTrue)
   122  		c.Check(strings.Contains(gotURL.String(), v.Version), jc.IsTrue)
   123  	}
   124  }
   125  
   126  func (*Suite) TestFetchSingleDefaultFilter(c *gc.C) {
   127  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
   128  	ts := httptest.NewServer(&sstreamsHandler{})
   129  	defer ts.Close()
   130  	tds := []simplestreams.DataSource{
   131  		newTestDataSource(ss, ts.URL)}
   132  	constraints := &imagemetadata.ImageConstraint{
   133  		LookupParams: simplestreams.LookupParams{
   134  			Arches:   []string{"ppc64el"},
   135  			Releases: []string{"16.04"},
   136  		}}
   137  	got, resolveInfo, err := Fetch(ss, tds, constraints, nil)
   138  	c.Check(resolveInfo.Signed, jc.IsTrue)
   139  	c.Check(err, jc.ErrorIsNil)
   140  	c.Assert(len(got), jc.DeepEquals, 8)
   141  	c.Check(got[0].Arch, jc.DeepEquals, "ppc64el")
   142  	c.Check(err, jc.ErrorIsNil)
   143  	for _, v := range got {
   144  		_, err := v.DownloadURL(ts.URL)
   145  		c.Check(err, jc.ErrorIsNil)
   146  	}
   147  }
   148  
   149  func (*Suite) TestFetchOneWithFilter(c *gc.C) {
   150  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
   151  	ts := httptest.NewServer(&sstreamsHandler{})
   152  	defer ts.Close()
   153  	tds := []simplestreams.DataSource{
   154  		newTestDataSource(ss, ts.URL)}
   155  	constraints := &imagemetadata.ImageConstraint{
   156  		LookupParams: simplestreams.LookupParams{
   157  			Arches:   []string{"ppc64el"},
   158  			Releases: []string{"16.04"},
   159  		}}
   160  	got, resolveInfo, err := Fetch(ss, tds, constraints, Filter("disk1.img"))
   161  	c.Check(resolveInfo.Signed, jc.IsTrue)
   162  	c.Check(err, jc.ErrorIsNil)
   163  	c.Assert(len(got), jc.DeepEquals, 1)
   164  	c.Check(got[0].Arch, jc.DeepEquals, "ppc64el")
   165  	// Assuming that the operator has not overridden the image download URL
   166  	// parameter we pass the default empty value which should fall back to
   167  	// the default cloud-images.ubuntu.com URL.
   168  	gotURL, err := got[0].DownloadURL("")
   169  	c.Assert(err, jc.ErrorIsNil)
   170  	c.Assert(
   171  		gotURL.String(),
   172  		jc.DeepEquals,
   173  		"http://cloud-images.ubuntu.com/server/releases/xenial/release-20211001/ubuntu-16.04-server-cloudimg-ppc64el-disk1.img")
   174  }
   175  
   176  func (*Suite) TestFetchManyWithFilter(c *gc.C) {
   177  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
   178  	ts := httptest.NewServer(&sstreamsHandler{})
   179  	defer ts.Close()
   180  	tds := []simplestreams.DataSource{
   181  		newTestDataSource(ss, ts.URL)}
   182  	constraints := &imagemetadata.ImageConstraint{
   183  		LookupParams: simplestreams.LookupParams{
   184  			Arches:   []string{"amd64", "arm64", "ppc64el"},
   185  			Releases: []string{"16.04"},
   186  		}}
   187  	got, resolveInfo, err := Fetch(ss, tds, constraints, Filter("disk1.img"))
   188  	c.Check(resolveInfo.Signed, jc.IsTrue)
   189  	c.Check(err, jc.ErrorIsNil)
   190  	c.Assert(len(got), jc.DeepEquals, 3)
   191  	c.Check(got[0].Arch, jc.DeepEquals, "amd64")
   192  	c.Check(got[1].Arch, jc.DeepEquals, "arm64")
   193  	c.Check(got[2].Arch, jc.DeepEquals, "ppc64el")
   194  	for i, arch := range []string{"amd64", "arm64", "ppc64el"} {
   195  		wantURL := fmt.Sprintf("http://cloud-images.ubuntu.com/server/releases/xenial/release-20211001/ubuntu-16.04-server-cloudimg-%s-disk1.img", arch)
   196  		// Assuming that the operator has not overridden the image
   197  		// download URL parameter we pass the default empty value which
   198  		// should fall back to the default cloud-images.ubuntu.com URL.
   199  		gotURL, err := got[i].DownloadURL("")
   200  		c.Check(err, jc.ErrorIsNil)
   201  		c.Check(gotURL.String(), jc.DeepEquals, wantURL)
   202  	}
   203  }
   204  
   205  func (*Suite) TestOneAmd64XenialTarGz(c *gc.C) {
   206  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
   207  	ts := httptest.NewServer(&sstreamsHandler{})
   208  	defer ts.Close()
   209  	got, err := One(ss, "amd64", "16.04", "", "tar.gz", newTestDataSourceFunc(ts.URL))
   210  	c.Check(err, jc.ErrorIsNil)
   211  	c.Assert(got, jc.DeepEquals, &Metadata{
   212  		Arch:    "amd64",
   213  		Release: "xenial",
   214  		Version: "16.04",
   215  		FType:   "tar.gz",
   216  		SHA256:  "c48036699274351be132f2aec7fec9fd2da936b6b512c65b2d9fd6531e5623ea",
   217  		Path:    "server/releases/xenial/release-20211001/ubuntu-16.04-server-cloudimg-amd64.tar.gz",
   218  		Size:    287684992,
   219  	})
   220  }
   221  
   222  func (*Suite) TestOneArm64JammyImg(c *gc.C) {
   223  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
   224  	ts := httptest.NewServer(&sstreamsHandler{})
   225  	defer ts.Close()
   226  	got, err := One(ss, "arm64", "22.04", "released", "disk1.img", newTestDataSourceFunc(ts.URL))
   227  	c.Check(err, jc.ErrorIsNil)
   228  	c.Assert(got, jc.DeepEquals, &Metadata{
   229  		Arch:    "arm64",
   230  		Release: "jammy",
   231  		Version: "22.04",
   232  		FType:   "disk1.img",
   233  		SHA256:  "78b5ca0da456b228e2441bdca0cca1eab30b1b6a3eaf9594eabcb2cfc21275f3",
   234  		Path:    "server/releases/jammy/release-20220923/ubuntu-22.04-server-cloudimg-arm64.img",
   235  		Size:    642646016,
   236  	})
   237  }
   238  
   239  func (*Suite) TestOneArm64FocalImg(c *gc.C) {
   240  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
   241  	ts := httptest.NewServer(&sstreamsHandler{})
   242  	defer ts.Close()
   243  	got, err := One(ss, "arm64", "20.04", "released", "disk1.img", newTestDataSourceFunc(ts.URL))
   244  	c.Check(err, jc.ErrorIsNil)
   245  	c.Assert(got, jc.DeepEquals, &Metadata{
   246  		Arch:    "arm64",
   247  		Release: "focal",
   248  		Version: "20.04",
   249  		FType:   "disk1.img",
   250  		SHA256:  "b8176161962c4f54e59366444bb696e92406823f643ed7bdcdd3d15d38dc0d53",
   251  		Path:    "server/releases/focal/release-20221003/ubuntu-20.04-server-cloudimg-arm64.img",
   252  		Size:    569901056,
   253  	})
   254  }
   255  
   256  func (*Suite) TestOneErrors(c *gc.C) {
   257  	ss := simplestreams.NewSimpleStreams(streamstesting.TestDataSourceFactory())
   258  	table := []struct {
   259  		description, arch, version, stream, ftype, errorMatch string
   260  	}{
   261  		{"empty arch", "", "20.04", "", "disk1.img", `invalid parameters supplied arch=""`},
   262  		{"invalid arch", "<F7>", "20.04", "", "disk1.img", `invalid parameters supplied arch="<F7>"`},
   263  		{"empty series", "arm64", "", "released", "disk1.img", `invalid parameters supplied version=""`},
   264  		{"invalid series", "amd64", "rusty", "", "disk1.img", `invalid parameters supplied version="rusty"`},
   265  		{"empty ftype", "ppc64el", "20.04", "", "", `invalid parameters supplied ftype=""`},
   266  		{"invalid file type", "amd64", "22.04", "", "tragedy", `invalid parameters supplied ftype="tragedy"`},
   267  		{"all wrong except stream", "a", "t", "", "y", `invalid parameters supplied arch="a" version="t" ftype="y"`},
   268  		{"stream not found", "amd64", "22.04", "hourly", "disk1.img", `no results for "amd64", "22.04", "hourly", "disk1.img"`},
   269  	}
   270  	ts := httptest.NewServer(&sstreamsHandler{})
   271  	defer ts.Close()
   272  	for i, test := range table {
   273  		c.Logf("test % 1d: %s\n", i+1, test.description)
   274  		_, err := One(ss, test.arch, test.version, test.stream, test.ftype, newTestDataSourceFunc(ts.URL))
   275  		c.Check(err, gc.ErrorMatches, test.errorMatch)
   276  	}
   277  }
   278  
   279  type sstreamsHandler struct{}
   280  
   281  func (h sstreamsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   282  	switch r.URL.Path {
   283  	case "/releases/streams/v1/index.sjson":
   284  		w.Header().Set("Content-Type", "application/json")
   285  		http.ServeFile(w, r, "testdata/index.sjson")
   286  		return
   287  	case "/releases/streams/v1/com.ubuntu.cloud:released:download.sjson":
   288  		w.Header().Set("Content-Type", "application/json")
   289  		http.ServeFile(w, r, "testdata/com.ubuntu.cloud-released-download.sjson")
   290  		return
   291  	default:
   292  		http.Error(w, r.URL.Path, 404)
   293  		return
   294  	}
   295  }