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

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package imagemetadata_test
     5  
     6  import (
     7  	"bytes"
     8  	"flag"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/http/httptest"
    13  	"reflect"
    14  	"strings"
    15  	stdtesting "testing"
    16  
    17  	"github.com/aws/aws-sdk-go-v2/service/ec2"
    18  	"github.com/juju/errors"
    19  	jc "github.com/juju/testing/checkers"
    20  	gc "gopkg.in/check.v1"
    21  
    22  	"github.com/juju/juju/environs/imagemetadata"
    23  	"github.com/juju/juju/environs/simplestreams"
    24  	sstesting "github.com/juju/juju/environs/simplestreams/testing"
    25  	"github.com/juju/juju/juju/keys"
    26  )
    27  
    28  var live = flag.Bool("live", false, "Include live simplestreams tests")
    29  var vendor = flag.String("vendor", "", "The vendor representing the source of the simplestream data")
    30  
    31  type liveTestData struct {
    32  	baseURL        string
    33  	requireSigned  bool
    34  	validCloudSpec simplestreams.CloudSpec
    35  }
    36  
    37  func getLiveURLs() (map[string]liveTestData, error) {
    38  	resolver := ec2.NewDefaultEndpointResolver()
    39  	ep, err := resolver.ResolveEndpoint("us-east-1", ec2.EndpointResolverOptions{})
    40  	if err != nil {
    41  		return nil, errors.Trace(err)
    42  	}
    43  
    44  	return map[string]liveTestData{
    45  		"ec2": {
    46  			baseURL:       imagemetadata.DefaultUbuntuBaseURL,
    47  			requireSigned: true,
    48  			validCloudSpec: simplestreams.CloudSpec{
    49  				Region:   "us-east-1",
    50  				Endpoint: ep.URL,
    51  			},
    52  		},
    53  		"canonistack": {
    54  			baseURL:       "https://swift.canonistack.canonical.com/v1/AUTH_a48765cc0e864be980ee21ae26aaaed4/simplestreams/data",
    55  			requireSigned: false,
    56  			validCloudSpec: simplestreams.CloudSpec{
    57  				Region:   "lcy01",
    58  				Endpoint: "https://keystone.canonistack.canonical.com:443/v1.0/",
    59  			},
    60  		},
    61  	}, nil
    62  }
    63  
    64  func Test(t *stdtesting.T) {
    65  	if *live {
    66  		if *vendor == "" {
    67  			t.Fatal("missing vendor")
    68  		}
    69  		var ok bool
    70  		var testData liveTestData
    71  		liveURLs, err := getLiveURLs()
    72  		if err != nil {
    73  			t.Fatalf(err.Error())
    74  		}
    75  		if testData, ok = liveURLs[*vendor]; !ok {
    76  			keys := reflect.ValueOf(liveURLs).MapKeys()
    77  			t.Fatalf("Unknown vendor %s. Must be one of %s", *vendor, keys)
    78  		}
    79  		cons, err := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
    80  			CloudSpec: testData.validCloudSpec,
    81  			Releases:  []string{"12.10"},
    82  			Arches:    []string{"amd64"},
    83  		})
    84  		if err != nil {
    85  			t.Fatalf(err.Error())
    86  		}
    87  		registerLiveSimpleStreamsTests(testData.baseURL, cons, testData.requireSigned)
    88  	}
    89  	registerSimpleStreamsTests(t)
    90  	gc.TestingT(t)
    91  }
    92  
    93  func registerSimpleStreamsTests(t *stdtesting.T) {
    94  	cons, err := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
    95  		CloudSpec: simplestreams.CloudSpec{
    96  			Region:   "us-east-1",
    97  			Endpoint: "https://ec2.us-east-1.amazonaws.com",
    98  		},
    99  		Releases: []string{"12.04"},
   100  		Arches:   []string{"amd64", "arm"},
   101  	})
   102  	if err != nil {
   103  		t.Fatalf(err.Error())
   104  	}
   105  	gc.Suite(&simplestreamsSuite{
   106  		LocalLiveSimplestreamsSuite: sstesting.LocalLiveSimplestreamsSuite{
   107  			Source:          sstesting.VerifyDefaultCloudDataSource("test roundtripper", "test:"),
   108  			RequireSigned:   false,
   109  			DataType:        imagemetadata.ImageIds,
   110  			StreamsVersion:  imagemetadata.CurrentStreamsVersion,
   111  			ValidConstraint: cons,
   112  		},
   113  	})
   114  	gc.Suite(&signedSuite{})
   115  }
   116  
   117  func registerLiveSimpleStreamsTests(baseURL string, validImageConstraint simplestreams.LookupConstraint, requireSigned bool) {
   118  	ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory())
   119  	gc.Suite(&sstesting.LocalLiveSimplestreamsSuite{
   120  		Source: ss.NewDataSource(simplestreams.Config{
   121  			Description:          "test",
   122  			BaseURL:              baseURL,
   123  			HostnameVerification: true,
   124  			Priority:             simplestreams.DEFAULT_CLOUD_DATA,
   125  			RequireSigned:        requireSigned,
   126  		}),
   127  		RequireSigned:   requireSigned,
   128  		DataType:        imagemetadata.ImageIds,
   129  		ValidConstraint: validImageConstraint,
   130  	})
   131  }
   132  
   133  type simplestreamsSuite struct {
   134  	sstesting.LocalLiveSimplestreamsSuite
   135  	sstesting.TestDataSuite
   136  }
   137  
   138  func (s *simplestreamsSuite) SetUpSuite(c *gc.C) {
   139  	s.LocalLiveSimplestreamsSuite.SetUpSuite(c)
   140  	s.TestDataSuite.SetUpSuite(c)
   141  }
   142  
   143  func (s *simplestreamsSuite) TearDownSuite(c *gc.C) {
   144  	s.TestDataSuite.TearDownSuite(c)
   145  	s.LocalLiveSimplestreamsSuite.TearDownSuite(c)
   146  }
   147  
   148  func (s *simplestreamsSuite) TestOfficialSources(c *gc.C) {
   149  	ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory())
   150  	s.PatchValue(&keys.JujuPublicKey, sstesting.SignedMetadataPublicKey)
   151  	origKey := imagemetadata.SetSigningPublicKey(sstesting.SignedMetadataPublicKey)
   152  	defer func() {
   153  		imagemetadata.SetSigningPublicKey(origKey)
   154  	}()
   155  	ds, err := imagemetadata.OfficialDataSources(ss, "daily")
   156  	c.Assert(err, jc.ErrorIsNil)
   157  	c.Assert(ds, gc.HasLen, 1)
   158  	url, err := ds[0].URL("")
   159  	c.Assert(err, jc.ErrorIsNil)
   160  	c.Assert(url, gc.Equals, "http://cloud-images.ubuntu.com/daily/")
   161  	c.Assert(ds[0].PublicSigningKey(), gc.Equals, sstesting.SignedMetadataPublicKey)
   162  }
   163  
   164  var fetchTests = []struct {
   165  	region  string
   166  	version string
   167  	arches  []string
   168  	images  []*imagemetadata.ImageMetadata
   169  }{
   170  	{
   171  		region:  "us-east-1",
   172  		version: "12.04",
   173  		arches:  []string{"amd64", "arm"},
   174  		images: []*imagemetadata.ImageMetadata{
   175  			{
   176  				Id:         "ami-442ea674",
   177  				VirtType:   "hvm",
   178  				Arch:       "amd64",
   179  				RegionName: "us-east-1",
   180  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   181  				Storage:    "ebs",
   182  			},
   183  			{
   184  				Id:         "ami-442ea684",
   185  				VirtType:   "pv",
   186  				Arch:       "amd64",
   187  				RegionName: "us-east-1",
   188  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   189  				Storage:    "instance",
   190  			},
   191  			{
   192  				Id:         "ami-442ea699",
   193  				VirtType:   "pv",
   194  				Arch:       "arm",
   195  				RegionName: "us-east-1",
   196  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   197  				Storage:    "ebs",
   198  			},
   199  		},
   200  	},
   201  	{
   202  		region:  "us-east-1",
   203  		version: "12.04",
   204  		arches:  []string{"amd64"},
   205  		images: []*imagemetadata.ImageMetadata{
   206  			{
   207  				Id:         "ami-442ea674",
   208  				VirtType:   "hvm",
   209  				Arch:       "amd64",
   210  				RegionName: "us-east-1",
   211  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   212  				Storage:    "ebs",
   213  			},
   214  			{
   215  				Id:         "ami-442ea684",
   216  				VirtType:   "pv",
   217  				Arch:       "amd64",
   218  				RegionName: "us-east-1",
   219  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   220  				Storage:    "instance",
   221  			},
   222  		},
   223  	},
   224  	{
   225  		region:  "us-east-1",
   226  		version: "12.04",
   227  		arches:  []string{"arm"},
   228  		images: []*imagemetadata.ImageMetadata{
   229  			{
   230  				Id:         "ami-442ea699",
   231  				VirtType:   "pv",
   232  				Arch:       "arm",
   233  				RegionName: "us-east-1",
   234  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   235  				Storage:    "ebs",
   236  			},
   237  		},
   238  	},
   239  	{
   240  		region:  "us-east-1",
   241  		version: "12.04",
   242  		arches:  []string{"amd64"},
   243  		images: []*imagemetadata.ImageMetadata{
   244  			{
   245  				Id:         "ami-442ea674",
   246  				VirtType:   "hvm",
   247  				Arch:       "amd64",
   248  				RegionName: "us-east-1",
   249  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   250  				Storage:    "ebs",
   251  			},
   252  			{
   253  				Id:         "ami-442ea684",
   254  				VirtType:   "pv",
   255  				Arch:       "amd64",
   256  				RegionName: "us-east-1",
   257  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   258  				Storage:    "instance",
   259  			},
   260  		},
   261  	},
   262  	{
   263  		version: "12.04",
   264  		arches:  []string{"amd64"},
   265  		images: []*imagemetadata.ImageMetadata{
   266  			{
   267  				Id:         "ami-26745463",
   268  				VirtType:   "pv",
   269  				Arch:       "amd64",
   270  				RegionName: "au-east-2",
   271  				Endpoint:   "https://somewhere-else",
   272  				Storage:    "ebs",
   273  			},
   274  			{
   275  				Id:         "ami-26745464",
   276  				VirtType:   "pv",
   277  				Arch:       "amd64",
   278  				RegionName: "au-east-1",
   279  				Endpoint:   "https://somewhere",
   280  				Storage:    "ebs",
   281  			},
   282  			{
   283  				Id:         "ami-442ea674",
   284  				VirtType:   "hvm",
   285  				Arch:       "amd64",
   286  				RegionName: "us-east-1",
   287  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   288  				Storage:    "ebs",
   289  			},
   290  			{
   291  				Id:          "ami-442ea675",
   292  				VirtType:    "hvm",
   293  				Arch:        "amd64",
   294  				RegionAlias: "uswest3",
   295  				RegionName:  "us-west-3",
   296  				Endpoint:    "https://ec2.us-west-3.amazonaws.com",
   297  				Storage:     "ebs",
   298  			},
   299  			{
   300  				Id:         "ami-442ea684",
   301  				VirtType:   "pv",
   302  				Arch:       "amd64",
   303  				RegionName: "us-east-1",
   304  				Endpoint:   "https://ec2.us-east-1.amazonaws.com",
   305  				Storage:    "instance",
   306  			},
   307  		},
   308  	},
   309  }
   310  
   311  func (s *simplestreamsSuite) TestFetch(c *gc.C) {
   312  	ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory())
   313  	for i, t := range fetchTests {
   314  		c.Logf("test %d", i)
   315  		cloudSpec := simplestreams.CloudSpec{
   316  			Region:   t.region,
   317  			Endpoint: "https://ec2.us-east-1.amazonaws.com",
   318  		}
   319  		if t.region == "" {
   320  			cloudSpec = simplestreams.EmptyCloudSpec
   321  		}
   322  		imageConstraint, err := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
   323  			CloudSpec: cloudSpec,
   324  			Releases:  []string{"12.04"},
   325  			Arches:    t.arches,
   326  		})
   327  		c.Assert(err, jc.ErrorIsNil)
   328  		// Add invalid datasource and check later that resolveInfo is correct.
   329  		invalidSource := sstesting.InvalidDataSource(s.RequireSigned)
   330  		images, resolveInfo, err := imagemetadata.Fetch(ss,
   331  			[]simplestreams.DataSource{invalidSource, s.Source}, imageConstraint)
   332  		if !c.Check(err, jc.ErrorIsNil) {
   333  			continue
   334  		}
   335  		for _, testImage := range t.images {
   336  			testImage.Version = t.version
   337  		}
   338  		c.Check(images, gc.DeepEquals, t.images)
   339  		c.Check(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{
   340  			Source:    "test roundtripper",
   341  			Signed:    s.RequireSigned,
   342  			IndexURL:  "test:/streams/v1/index.json",
   343  			MirrorURL: "",
   344  		})
   345  	}
   346  }
   347  
   348  type productSpecSuite struct{}
   349  
   350  var _ = gc.Suite(&productSpecSuite{})
   351  
   352  func (s *productSpecSuite) TestIdWithDefaultStream(c *gc.C) {
   353  	imageConstraint, err := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
   354  		Releases: []string{"12.04"},
   355  		Arches:   []string{"amd64"},
   356  	})
   357  	c.Assert(err, jc.ErrorIsNil)
   358  	for _, stream := range []string{"", "released"} {
   359  		imageConstraint.Stream = stream
   360  		ids, err := imageConstraint.ProductIds()
   361  		c.Assert(err, jc.ErrorIsNil)
   362  		c.Assert(ids, gc.DeepEquals, []string{"com.ubuntu.cloud:server:12.04:amd64"})
   363  	}
   364  }
   365  
   366  func (s *productSpecSuite) TestId(c *gc.C) {
   367  	imageConstraint, err := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
   368  		Releases: []string{"12.04"},
   369  		Arches:   []string{"amd64"},
   370  		Stream:   "daily",
   371  	})
   372  	c.Assert(err, jc.ErrorIsNil)
   373  	ids, err := imageConstraint.ProductIds()
   374  	c.Assert(err, jc.ErrorIsNil)
   375  	c.Assert(ids, gc.DeepEquals, []string{"com.ubuntu.cloud.daily:server:12.04:amd64"})
   376  }
   377  
   378  func (s *productSpecSuite) TestIdMultiArch(c *gc.C) {
   379  	imageConstraint, err := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
   380  		Releases: []string{"12.04"},
   381  		Arches:   []string{"amd64", "arm64"},
   382  		Stream:   "daily",
   383  	})
   384  	c.Assert(err, jc.ErrorIsNil)
   385  	ids, err := imageConstraint.ProductIds()
   386  	c.Assert(err, jc.ErrorIsNil)
   387  	c.Assert(ids, gc.DeepEquals, []string{
   388  		"com.ubuntu.cloud.daily:server:12.04:amd64",
   389  		"com.ubuntu.cloud.daily:server:12.04:arm64"})
   390  }
   391  
   392  type signedSuite struct {
   393  	origKey string
   394  	server  *httptest.Server
   395  }
   396  
   397  func (s *signedSuite) SetUpSuite(_ *gc.C) {
   398  	s.origKey = imagemetadata.SetSigningPublicKey(sstesting.SignedMetadataPublicKey)
   399  	s.server = httptest.NewServer(&sstreamsHandler{})
   400  }
   401  
   402  func (s *signedSuite) TearDownSuite(_ *gc.C) {
   403  	s.server.Close()
   404  	imagemetadata.SetSigningPublicKey(s.origKey)
   405  }
   406  
   407  func (s *signedSuite) TestSignedImageMetadata(c *gc.C) {
   408  	ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory())
   409  	signedSource := simplestreams.NewDataSource(
   410  		simplestreams.Config{
   411  			Description:          "test",
   412  			BaseURL:              fmt.Sprintf("%s/signed", s.server.URL),
   413  			PublicSigningKey:     sstesting.SignedMetadataPublicKey,
   414  			HostnameVerification: true,
   415  			Priority:             simplestreams.DEFAULT_CLOUD_DATA,
   416  			RequireSigned:        true,
   417  		},
   418  	)
   419  	imageConstraint, err := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
   420  		CloudSpec: simplestreams.CloudSpec{
   421  			Region:   "us-east-1",
   422  			Endpoint: "https://ec2.us-east-1.amazonaws.com",
   423  		},
   424  		Releases: []string{"12.04"},
   425  		Arches:   []string{"amd64"},
   426  	})
   427  	c.Assert(err, jc.ErrorIsNil)
   428  	images, resolveInfo, err := imagemetadata.Fetch(ss, []simplestreams.DataSource{signedSource}, imageConstraint)
   429  	c.Assert(err, jc.ErrorIsNil)
   430  	c.Assert(len(images), gc.Equals, 1)
   431  	c.Assert(images[0].Id, gc.Equals, "ami-123456")
   432  	c.Check(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{
   433  		Source:    "test",
   434  		Signed:    true,
   435  		IndexURL:  fmt.Sprintf("%s/signed/streams/v1/index.sjson", s.server.URL),
   436  		MirrorURL: "",
   437  	})
   438  }
   439  
   440  func (s *signedSuite) TestSignedImageMetadataInvalidSignature(c *gc.C) {
   441  	ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory())
   442  	signedSource := simplestreams.NewDataSource(simplestreams.Config{
   443  		Description:          "test",
   444  		BaseURL:              fmt.Sprintf("%s/signed", s.server.URL),
   445  		HostnameVerification: true,
   446  		Priority:             simplestreams.DEFAULT_CLOUD_DATA,
   447  		RequireSigned:        true,
   448  	})
   449  	imageConstraint, err := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
   450  		CloudSpec: simplestreams.CloudSpec{
   451  			Region:   "us-east-1",
   452  			Endpoint: "https://ec2.us-east-1.amazonaws.com",
   453  		},
   454  		Releases: []string{"12.04"},
   455  		Arches:   []string{"amd64"},
   456  	})
   457  	c.Assert(err, jc.ErrorIsNil)
   458  	imagemetadata.SetSigningPublicKey(s.origKey)
   459  	_, _, err = imagemetadata.Fetch(ss, []simplestreams.DataSource{signedSource}, imageConstraint)
   460  	c.Assert(err, gc.ErrorMatches, "cannot read index data.*")
   461  }
   462  
   463  type sstreamsHandler struct{}
   464  
   465  func (h *sstreamsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   466  	switch r.URL.Path {
   467  	case "/unsigned/streams/v1/index.json":
   468  		w.Header().Set("Content-Type", "application/json")
   469  		w.WriteHeader(http.StatusOK)
   470  		_, _ = io.WriteString(w, unsignedIndex)
   471  	case "/unsigned/streams/v1/image_metadata.json":
   472  		w.Header().Set("Content-Type", "application/json")
   473  		w.WriteHeader(http.StatusOK)
   474  		_, _ = io.WriteString(w, unsignedProduct)
   475  	case "/signed/streams/v1/image_metadata.sjson":
   476  		w.Header().Set("Content-Type", "application/json")
   477  		w.WriteHeader(http.StatusOK)
   478  		rawUnsignedProduct := strings.Replace(
   479  			unsignedProduct, "ami-26745463", "ami-123456", -1)
   480  		_, _ = io.WriteString(w, encode(rawUnsignedProduct))
   481  		return
   482  	case "/signed/streams/v1/index.sjson":
   483  		w.Header().Set("Content-Type", "application/json")
   484  		w.WriteHeader(http.StatusOK)
   485  		rawUnsignedIndex := strings.Replace(
   486  			unsignedIndex, "streams/v1/image_metadata.json", "streams/v1/image_metadata.sjson", -1)
   487  		_, _ = io.WriteString(w, encode(rawUnsignedIndex))
   488  		return
   489  	default:
   490  		http.Error(w, r.URL.Path, 404)
   491  		return
   492  	}
   493  }
   494  
   495  func encode(data string) string {
   496  	reader := bytes.NewReader([]byte(data))
   497  	signedData, _ := simplestreams.Encode(
   498  		reader, sstesting.SignedMetadataPrivateKey, sstesting.PrivateKeyPassphrase)
   499  	return string(signedData)
   500  }
   501  
   502  var unsignedIndex = `
   503  {
   504   "index": {
   505    "com.ubuntu.cloud:released:precise": {
   506     "updated": "Wed, 01 May 2013 13:31:26 +0000",
   507     "clouds": [
   508  	{
   509  	 "region": "us-east-1",
   510  	 "endpoint": "https://ec2.us-east-1.amazonaws.com"
   511  	}
   512     ],
   513     "cloudname": "aws",
   514     "datatype": "image-ids",
   515     "format": "products:1.0",
   516     "products": [
   517  	"com.ubuntu.cloud:server:12.04:amd64"
   518     ],
   519     "path": "streams/v1/image_metadata.json"
   520    }
   521   },
   522   "updated": "Wed, 01 May 2013 13:31:26 +0000",
   523   "format": "index:1.0"
   524  }
   525  `
   526  var unsignedProduct = `
   527  {
   528   "updated": "Wed, 01 May 2013 13:31:26 +0000",
   529   "content_id": "com.ubuntu.cloud:released:aws",
   530   "products": {
   531    "com.ubuntu.cloud:server:12.04:amd64": {
   532     "release": "12.04",
   533     "version": "12.04",
   534     "arch": "amd64",
   535     "region": "us-east-1",
   536     "endpoint": "https://somewhere",
   537     "versions": {
   538      "20121218": {
   539       "region": "us-east-1",
   540       "endpoint": "https://somewhere-else",
   541       "items": {
   542        "usww1pe": {
   543         "root_store": "ebs",
   544         "virt": "pv",
   545         "id": "ami-26745463"
   546        }
   547       },
   548       "pubname": "ubuntu-precise-12.04-amd64-server-20121218",
   549       "label": "release"
   550      }
   551     }
   552    }
   553   },
   554   "format": "products:1.0"
   555  }
   556  `