github.com/dean7474/operator-registry@v1.21.1-0.20220418203638-d4717f98c2e5/pkg/server/server_test.go (about)

     1  package server
     2  
     3  import (
     4  	"io"
     5  	"io/ioutil"
     6  	"net"
     7  	"os"
     8  	"path/filepath"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/google/go-cmp/cmp/cmpopts"
    14  	"github.com/sirupsen/logrus"
    15  	"github.com/stretchr/testify/require"
    16  	"golang.org/x/net/context"
    17  	"google.golang.org/grpc"
    18  	"google.golang.org/grpc/connectivity"
    19  
    20  	"github.com/operator-framework/operator-registry/pkg/api"
    21  	"github.com/operator-framework/operator-registry/pkg/registry"
    22  	"github.com/operator-framework/operator-registry/pkg/sqlite"
    23  )
    24  
    25  const (
    26  	dbPort    = ":50052"
    27  	dbAddress = "localhost" + dbPort
    28  	dbName    = "test.db"
    29  
    30  	cfgPort    = ":50053"
    31  	cfgAddress = "localhost" + cfgPort
    32  )
    33  
    34  func dbStore(dbPath string) *sqlite.SQLQuerier {
    35  	_ = os.Remove(dbPath)
    36  
    37  	db, err := sqlite.Open(dbPath)
    38  	if err != nil {
    39  		logrus.Fatal(err)
    40  	}
    41  	load, err := sqlite.NewSQLLiteLoader(db)
    42  	if err != nil {
    43  		logrus.Fatal(err)
    44  	}
    45  	if err := load.Migrate(context.TODO()); err != nil {
    46  		logrus.Fatal(err)
    47  	}
    48  
    49  	loader := sqlite.NewSQLLoaderForDirectory(load, "../../manifests")
    50  	if err := loader.Populate(); err != nil {
    51  		logrus.Fatal(err)
    52  	}
    53  	if err := db.Close(); err != nil {
    54  		logrus.Fatal(err)
    55  	}
    56  	store, err := sqlite.NewSQLLiteQuerier(dbPath)
    57  	if err != nil {
    58  		logrus.Fatal(err)
    59  	}
    60  	return store
    61  }
    62  
    63  func cfgStore() (*registry.Querier, error) {
    64  	tmpDir, err := ioutil.TempDir("", "server_test-")
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	defer os.RemoveAll(tmpDir)
    69  
    70  	dbFile := filepath.Join(tmpDir, "test.db")
    71  
    72  	dbStore := dbStore(dbFile)
    73  	m, err := sqlite.ToModel(context.TODO(), dbStore)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	store, err := registry.NewQuerier(m)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	return store, nil
    82  }
    83  
    84  func server(store registry.GRPCQuery) *grpc.Server {
    85  	s := grpc.NewServer()
    86  	api.RegisterRegistryServer(s, NewRegistryServer(store))
    87  	return s
    88  }
    89  
    90  func TestMain(m *testing.M) {
    91  	s1 := server(dbStore(dbName))
    92  
    93  	cfgQuerier, err := cfgStore()
    94  	defer cfgQuerier.Close()
    95  	if err != nil {
    96  		logrus.Fatalf("failed to create fbc querier: %v", err)
    97  	}
    98  	s2 := server(cfgQuerier)
    99  	go func() {
   100  		lis, err := net.Listen("tcp", dbPort)
   101  		if err != nil {
   102  			logrus.Fatalf("failed to listen: %v", err)
   103  		}
   104  		if err := s1.Serve(lis); err != nil {
   105  			logrus.Fatalf("failed to serve db: %v", err)
   106  		}
   107  	}()
   108  	go func() {
   109  		lis, err := net.Listen("tcp", cfgPort)
   110  		if err != nil {
   111  			logrus.Fatalf("failed to listen: %v", err)
   112  		}
   113  		if err := s2.Serve(lis); err != nil {
   114  			logrus.Fatalf("failed to serve configs: %v", err)
   115  		}
   116  	}()
   117  	exit := m.Run()
   118  	if err := os.Remove(dbName); err != nil {
   119  		logrus.Fatalf("couldn't remove db")
   120  	}
   121  	os.Exit(exit)
   122  }
   123  
   124  func client(t *testing.T, address string) (api.RegistryClient, *grpc.ClientConn) {
   125  	conn, err := grpc.Dial(address, grpc.WithInsecure())
   126  	if err != nil {
   127  		t.Fatalf("did not connect: %v", err)
   128  	}
   129  
   130  	ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
   131  	conn.WaitForStateChange(ctx, connectivity.TransientFailure)
   132  
   133  	return api.NewRegistryClient(conn), conn
   134  }
   135  
   136  func TestListPackages(t *testing.T) {
   137  	t.Run("Sqlite", testListPackages(dbAddress))
   138  	t.Run("DeclarativeConfig", testListPackages(cfgAddress))
   139  }
   140  
   141  func testListPackages(addr string) func(*testing.T) {
   142  	return func(t *testing.T) {
   143  		c, conn := client(t, addr)
   144  		defer conn.Close()
   145  
   146  		stream, err := c.ListPackages(context.TODO(), &api.ListPackageRequest{})
   147  		require.NoError(t, err)
   148  
   149  		packages := []string{}
   150  		waitc := make(chan struct{})
   151  		go func(t *testing.T) {
   152  			for {
   153  				in, err := stream.Recv()
   154  				if err == io.EOF {
   155  					// read done.
   156  					close(waitc)
   157  					return
   158  				}
   159  				require.NoError(t, err)
   160  				packages = append(packages, in.Name)
   161  			}
   162  		}(t)
   163  		<-waitc
   164  		require.ElementsMatch(t, []string{"etcd", "prometheus", "strimzi-kafka-operator"}, packages)
   165  	}
   166  }
   167  
   168  func TestGetPackage(t *testing.T) {
   169  	t.Run("Sqlite", testGetPackage(dbAddress))
   170  	t.Run("DeclarativeConfig", testGetPackage(cfgAddress))
   171  }
   172  
   173  func testGetPackage(addr string) func(*testing.T) {
   174  	return func(t *testing.T) {
   175  		c, conn := client(t, addr)
   176  		defer conn.Close()
   177  
   178  		pkg, err := c.GetPackage(context.TODO(), &api.GetPackageRequest{Name: "etcd"})
   179  		require.NoError(t, err)
   180  		expected := &api.Package{
   181  			Name: "etcd",
   182  			Channels: []*api.Channel{
   183  				{
   184  					Name:    "alpha",
   185  					CsvName: "etcdoperator.v0.9.2",
   186  				},
   187  				{
   188  					Name:    "beta",
   189  					CsvName: "etcdoperator.v0.9.0",
   190  				},
   191  				{
   192  					Name:    "stable",
   193  					CsvName: "etcdoperator.v0.9.2",
   194  				},
   195  			},
   196  			DefaultChannelName: "alpha",
   197  		}
   198  		opts := []cmp.Option{
   199  			cmpopts.IgnoreUnexported(api.Package{}),
   200  			cmpopts.IgnoreUnexported(api.Channel{}),
   201  			cmpopts.SortSlices(func(x, y *api.Channel) bool {
   202  				return x.Name < y.Name
   203  			}),
   204  		}
   205  		require.True(t, cmp.Equal(expected, pkg, opts...), cmp.Diff(expected, pkg, opts...))
   206  	}
   207  }
   208  
   209  func TestGetBundle(t *testing.T) {
   210  	t.Run("Sqlite", testGetBundle(dbAddress, etcdoperator_v0_9_2("alpha", false, false)))
   211  	t.Run("DeclarativeConfig", testGetBundle(cfgAddress, etcdoperator_v0_9_2("alpha", false, true)))
   212  }
   213  
   214  func testGetBundle(addr string, expected *api.Bundle) func(*testing.T) {
   215  	return func(t *testing.T) {
   216  		c, conn := client(t, addr)
   217  		defer conn.Close()
   218  
   219  		bundle, err := c.GetBundle(context.TODO(), &api.GetBundleRequest{PkgName: "etcd", ChannelName: "alpha", CsvName: "etcdoperator.v0.9.2"})
   220  		require.NoError(t, err)
   221  
   222  		EqualBundles(t, *expected, *bundle)
   223  	}
   224  }
   225  
   226  func TestGetBundleForChannel(t *testing.T) {
   227  	{
   228  		b := etcdoperator_v0_9_2("alpha", false, false)
   229  		t.Run("Sqlite", testGetBundleForChannel(dbAddress, &api.Bundle{
   230  			CsvName: b.CsvName,
   231  			CsvJson: b.CsvJson + "\n",
   232  		}))
   233  	}
   234  	t.Run("DeclarativeConfig", testGetBundleForChannel(cfgAddress, etcdoperator_v0_9_2("alpha", false, true)))
   235  }
   236  
   237  func testGetBundleForChannel(addr string, expected *api.Bundle) func(*testing.T) {
   238  	return func(t *testing.T) {
   239  		c, conn := client(t, addr)
   240  		defer conn.Close()
   241  
   242  		bundle, err := c.GetBundleForChannel(context.TODO(), &api.GetBundleInChannelRequest{PkgName: "etcd", ChannelName: "alpha"})
   243  		require.NoError(t, err)
   244  		EqualBundles(t, *expected, *bundle)
   245  	}
   246  }
   247  
   248  func TestGetChannelEntriesThatReplace(t *testing.T) {
   249  	t.Run("Sqlite", testGetChannelEntriesThatReplace(dbAddress))
   250  	t.Run("DeclarativeConfig", testGetChannelEntriesThatReplace(cfgAddress))
   251  }
   252  
   253  func testGetChannelEntriesThatReplace(addr string) func(*testing.T) {
   254  	return func(t *testing.T) {
   255  		c, conn := client(t, addr)
   256  		defer conn.Close()
   257  
   258  		stream, err := c.GetChannelEntriesThatReplace(context.TODO(), &api.GetAllReplacementsRequest{CsvName: "etcdoperator.v0.6.1"})
   259  		require.NoError(t, err)
   260  
   261  		channelEntries := []*api.ChannelEntry{}
   262  		waitc := make(chan struct{})
   263  		go func(t *testing.T) {
   264  			for {
   265  				in, err := stream.Recv()
   266  				if err == io.EOF {
   267  					// read done.
   268  					close(waitc)
   269  					return
   270  				}
   271  				if err != nil {
   272  					t.Error(err)
   273  					close(waitc)
   274  					return
   275  				}
   276  				channelEntries = append(channelEntries, in)
   277  			}
   278  		}(t)
   279  		<-waitc
   280  
   281  		expected := []*api.ChannelEntry{
   282  			{
   283  				PackageName: "etcd",
   284  				ChannelName: "alpha",
   285  				BundleName:  "etcdoperator.v0.9.0",
   286  				Replaces:    "etcdoperator.v0.6.1",
   287  			},
   288  			{
   289  				PackageName: "etcd",
   290  				ChannelName: "beta",
   291  				BundleName:  "etcdoperator.v0.9.0",
   292  				Replaces:    "etcdoperator.v0.6.1",
   293  			},
   294  			{
   295  				PackageName: "etcd",
   296  				ChannelName: "stable",
   297  				BundleName:  "etcdoperator.v0.9.0",
   298  				Replaces:    "etcdoperator.v0.6.1",
   299  			},
   300  		}
   301  		opts := []cmp.Option{
   302  			cmpopts.IgnoreUnexported(api.ChannelEntry{}),
   303  			cmpopts.SortSlices(func(x, y *api.ChannelEntry) bool {
   304  				if x.PackageName != y.PackageName {
   305  					return x.PackageName < y.PackageName
   306  				}
   307  				if x.ChannelName != y.ChannelName {
   308  					return x.ChannelName < y.ChannelName
   309  				}
   310  				if x.BundleName != y.BundleName {
   311  					return x.BundleName < y.BundleName
   312  				}
   313  				if x.Replaces != y.Replaces {
   314  					return x.Replaces < y.Replaces
   315  				}
   316  				return false
   317  			}),
   318  		}
   319  
   320  		require.Truef(t, cmp.Equal(expected, channelEntries, opts...), cmp.Diff(expected, channelEntries, opts...))
   321  	}
   322  }
   323  
   324  func TestGetBundleThatReplaces(t *testing.T) {
   325  	t.Run("Sqlite", testGetBundleThatReplaces(dbAddress, etcdoperator_v0_9_2("alpha", false, false)))
   326  	t.Run("DeclarativeConfig", testGetBundleThatReplaces(cfgAddress, etcdoperator_v0_9_2("alpha", false, true)))
   327  }
   328  
   329  func testGetBundleThatReplaces(addr string, expected *api.Bundle) func(*testing.T) {
   330  	return func(t *testing.T) {
   331  		c, conn := client(t, addr)
   332  		defer conn.Close()
   333  
   334  		bundle, err := c.GetBundleThatReplaces(context.TODO(), &api.GetReplacementRequest{CsvName: "etcdoperator.v0.9.0", PkgName: "etcd", ChannelName: "alpha"})
   335  		require.NoError(t, err)
   336  		EqualBundles(t, *expected, *bundle)
   337  	}
   338  }
   339  
   340  func TestGetBundleThatReplacesSynthetic(t *testing.T) {
   341  	t.Run("Sqlite", testGetBundleThatReplacesSynthetic(dbAddress, etcdoperator_v0_9_2("alpha", false, false)))
   342  	t.Run("DeclarativeConfig", testGetBundleThatReplacesSynthetic(cfgAddress, etcdoperator_v0_9_2("alpha", false, true)))
   343  }
   344  
   345  func testGetBundleThatReplacesSynthetic(addr string, expected *api.Bundle) func(*testing.T) {
   346  	return func(t *testing.T) {
   347  		c, conn := client(t, addr)
   348  		defer conn.Close()
   349  
   350  		// 0.9.1 is not actually a bundle in the registry
   351  		bundle, err := c.GetBundleThatReplaces(context.TODO(), &api.GetReplacementRequest{CsvName: "etcdoperator.v0.9.1", PkgName: "etcd", ChannelName: "alpha"})
   352  		require.NoError(t, err)
   353  		EqualBundles(t, *expected, *bundle)
   354  	}
   355  }
   356  
   357  func TestGetChannelEntriesThatProvide(t *testing.T) {
   358  	t.Run("Sqlite", testGetChannelEntriesThatProvide(dbAddress))
   359  	t.Run("DeclarativeConfig", testGetChannelEntriesThatProvide(cfgAddress))
   360  }
   361  
   362  func testGetChannelEntriesThatProvide(addr string) func(t *testing.T) {
   363  	return func(t *testing.T) {
   364  		c, conn := client(t, addr)
   365  		defer conn.Close()
   366  
   367  		stream, err := c.GetChannelEntriesThatProvide(context.TODO(), &api.GetAllProvidersRequest{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdCluster"})
   368  		require.NoError(t, err)
   369  
   370  		channelEntries := []api.ChannelEntry{}
   371  		waitc := make(chan struct{})
   372  		go func(t *testing.T) {
   373  			for {
   374  				in, err := stream.Recv()
   375  				if err == io.EOF {
   376  					// read done.
   377  					close(waitc)
   378  					return
   379  				}
   380  				if err != nil {
   381  					t.Error(err)
   382  					close(waitc)
   383  					return
   384  				}
   385  				channelEntries = append(channelEntries, *in)
   386  			}
   387  		}(t)
   388  		<-waitc
   389  
   390  		expected := []api.ChannelEntry{
   391  			{
   392  				PackageName: "etcd",
   393  				ChannelName: "alpha",
   394  				BundleName:  "etcdoperator.v0.6.1",
   395  				Replaces:    "",
   396  			},
   397  			{
   398  				PackageName: "etcd",
   399  				ChannelName: "alpha",
   400  				BundleName:  "etcdoperator.v0.9.0",
   401  				Replaces:    "etcdoperator.v0.6.1",
   402  			},
   403  			{
   404  				PackageName: "etcd",
   405  				ChannelName: "alpha",
   406  				BundleName:  "etcdoperator.v0.9.2",
   407  				Replaces:    "etcdoperator.v0.9.1",
   408  			},
   409  			{
   410  				PackageName: "etcd",
   411  				ChannelName: "alpha",
   412  				BundleName:  "etcdoperator.v0.9.2",
   413  				Replaces:    "etcdoperator.v0.9.0",
   414  			},
   415  			{
   416  				PackageName: "etcd",
   417  				ChannelName: "beta",
   418  				BundleName:  "etcdoperator.v0.6.1",
   419  				Replaces:    "",
   420  			},
   421  			{
   422  				PackageName: "etcd",
   423  				ChannelName: "beta",
   424  				BundleName:  "etcdoperator.v0.9.0",
   425  				Replaces:    "etcdoperator.v0.6.1",
   426  			},
   427  			{
   428  				PackageName: "etcd",
   429  				ChannelName: "stable",
   430  				BundleName:  "etcdoperator.v0.6.1",
   431  				Replaces:    "",
   432  			},
   433  			{
   434  				PackageName: "etcd",
   435  				ChannelName: "stable",
   436  				BundleName:  "etcdoperator.v0.9.0",
   437  				Replaces:    "etcdoperator.v0.6.1",
   438  			},
   439  			{
   440  				PackageName: "etcd",
   441  				ChannelName: "stable",
   442  				BundleName:  "etcdoperator.v0.9.2",
   443  				Replaces:    "etcdoperator.v0.9.1",
   444  			},
   445  			{
   446  				PackageName: "etcd",
   447  				ChannelName: "stable",
   448  				BundleName:  "etcdoperator.v0.9.2",
   449  				Replaces:    "etcdoperator.v0.9.0",
   450  			},
   451  		}
   452  		opts := []cmp.Option{
   453  			cmpopts.IgnoreUnexported(api.ChannelEntry{}),
   454  			cmpopts.SortSlices(func(x, y api.ChannelEntry) bool {
   455  				if x.PackageName != y.PackageName {
   456  					return x.PackageName < y.PackageName
   457  				}
   458  				if x.ChannelName != y.ChannelName {
   459  					return x.ChannelName < y.ChannelName
   460  				}
   461  				if x.BundleName != y.BundleName {
   462  					return x.BundleName < y.BundleName
   463  				}
   464  				if x.Replaces != y.Replaces {
   465  					return x.Replaces < y.Replaces
   466  				}
   467  				return false
   468  			}),
   469  		}
   470  		require.Truef(t, cmp.Equal(expected, channelEntries, opts...), cmp.Diff(expected, channelEntries, opts...))
   471  	}
   472  }
   473  
   474  func TestGetLatestChannelEntriesThatProvide(t *testing.T) {
   475  	t.Run("Sqlite", testGetLatestChannelEntriesThatProvide(dbAddress))
   476  	t.Run("DeclarativeConfig", testGetLatestChannelEntriesThatProvide(cfgAddress))
   477  }
   478  
   479  func testGetLatestChannelEntriesThatProvide(addr string) func(t *testing.T) {
   480  	return func(t *testing.T) {
   481  		c, conn := client(t, addr)
   482  		defer conn.Close()
   483  
   484  		stream, err := c.GetLatestChannelEntriesThatProvide(context.TODO(), &api.GetLatestProvidersRequest{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdCluster"})
   485  		require.NoError(t, err)
   486  
   487  		channelEntries := []*api.ChannelEntry{}
   488  		waitc := make(chan struct{})
   489  		go func(t *testing.T) {
   490  			for {
   491  				in, err := stream.Recv()
   492  				if err == io.EOF {
   493  					// read done.
   494  					close(waitc)
   495  					return
   496  				}
   497  				if err != nil {
   498  					t.Error(err)
   499  					close(waitc)
   500  					return
   501  				}
   502  				channelEntries = append(channelEntries, in)
   503  			}
   504  		}(t)
   505  		<-waitc
   506  
   507  		expected := []*api.ChannelEntry{
   508  			{
   509  				PackageName: "etcd",
   510  				ChannelName: "alpha",
   511  				BundleName:  "etcdoperator.v0.9.2",
   512  				Replaces:    "etcdoperator.v0.9.0",
   513  			},
   514  			{
   515  				PackageName: "etcd",
   516  				ChannelName: "beta",
   517  				BundleName:  "etcdoperator.v0.9.0",
   518  				Replaces:    "etcdoperator.v0.6.1",
   519  			},
   520  			{
   521  				PackageName: "etcd",
   522  				ChannelName: "stable",
   523  				BundleName:  "etcdoperator.v0.9.2",
   524  				Replaces:    "etcdoperator.v0.9.0",
   525  			},
   526  		}
   527  
   528  		opts := []cmp.Option{
   529  			cmpopts.IgnoreUnexported(api.ChannelEntry{}),
   530  			cmpopts.SortSlices(func(x, y *api.ChannelEntry) bool {
   531  				if x.PackageName != y.PackageName {
   532  					return x.PackageName < y.PackageName
   533  				}
   534  				if x.ChannelName != y.ChannelName {
   535  					return x.ChannelName < y.ChannelName
   536  				}
   537  				if x.BundleName != y.BundleName {
   538  					return x.BundleName < y.BundleName
   539  				}
   540  				if x.Replaces != y.Replaces {
   541  					return x.Replaces < y.Replaces
   542  				}
   543  				return false
   544  			}),
   545  		}
   546  		require.Truef(t, cmp.Equal(expected, channelEntries, opts...), cmp.Diff(expected, channelEntries, opts...))
   547  	}
   548  }
   549  
   550  func TestGetDefaultBundleThatProvides(t *testing.T) {
   551  	t.Run("Sqlite", testGetDefaultBundleThatProvides(dbAddress, etcdoperator_v0_9_2("alpha", false, false)))
   552  	t.Run("DeclarativeConfig", testGetDefaultBundleThatProvides(cfgAddress, etcdoperator_v0_9_2("alpha", false, true)))
   553  }
   554  
   555  func testGetDefaultBundleThatProvides(addr string, expected *api.Bundle) func(*testing.T) {
   556  	return func(t *testing.T) {
   557  		c, conn := client(t, addr)
   558  		defer conn.Close()
   559  
   560  		bundle, err := c.GetDefaultBundleThatProvides(context.TODO(), &api.GetDefaultProviderRequest{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdCluster"})
   561  		require.NoError(t, err)
   562  		EqualBundles(t, *expected, *bundle)
   563  	}
   564  }
   565  
   566  func TestListBundles(t *testing.T) {
   567  	t.Run("Sqlite", testListBundles(dbAddress,
   568  		etcdoperator_v0_9_2("alpha", true, false),
   569  		etcdoperator_v0_9_2("stable", true, false)))
   570  	t.Run("DeclarativeConfig", testListBundles(cfgAddress,
   571  		etcdoperator_v0_9_2("alpha", true, true),
   572  		etcdoperator_v0_9_2("stable", true, true)))
   573  }
   574  
   575  func testListBundles(addr string, etcdAlpha *api.Bundle, etcdStable *api.Bundle) func(*testing.T) {
   576  	return func(t *testing.T) {
   577  		require := require.New(t)
   578  
   579  		c, conn := client(t, addr)
   580  		defer conn.Close()
   581  
   582  		stream, err := c.ListBundles(context.TODO(), &api.ListBundlesRequest{})
   583  		require.NoError(err)
   584  
   585  		expected := []string{
   586  			"etcdoperator.v0.6.1",
   587  			"prometheusoperator.0.22.2",
   588  			"strimzi-cluster-operator.v0.11.0",
   589  			"strimzi-cluster-operator.v0.11.1",
   590  			"strimzi-cluster-operator.v0.12.2",
   591  			"etcdoperator.v0.9.0",
   592  			"prometheusoperator.0.15.0",
   593  			"prometheusoperator.0.14.0",
   594  			"etcdoperator.v0.6.1",
   595  			"etcdoperator.v0.6.1",
   596  			"etcdoperator.v0.9.0",
   597  			"strimzi-cluster-operator.v0.12.1",
   598  			"strimzi-cluster-operator.v0.11.0",
   599  			"etcdoperator.v0.9.2",
   600  			"etcdoperator.v0.9.2",
   601  			"strimzi-cluster-operator.v0.11.1",
   602  			"strimzi-cluster-operator.v0.11.0",
   603  			"strimzi-cluster-operator.v0.12.1",
   604  			"strimzi-cluster-operator.v0.11.1",
   605  			"etcdoperator.v0.9.0",
   606  		}
   607  
   608  		var names []string
   609  		var gotBundles = make([]*api.Bundle, 0)
   610  
   611  		waitc := make(chan struct{})
   612  		go func(t *testing.T) {
   613  			tt := t
   614  			for {
   615  				in, err := stream.Recv()
   616  
   617  				if err == io.EOF {
   618  					// read done.
   619  					close(waitc)
   620  					return
   621  				}
   622  				if err != nil {
   623  					tt.Error(err)
   624  					close(waitc)
   625  					return
   626  				}
   627  				names = append(names, in.CsvName)
   628  				if in.CsvName == etcdAlpha.CsvName {
   629  					gotBundles = append(gotBundles, in)
   630  				}
   631  			}
   632  		}(t)
   633  		<-waitc
   634  
   635  		require.ElementsMatch(expected, names, "%#v\n%#v", expected, names)
   636  
   637  		// TODO: this test needs better expectations
   638  		// check that one of the entries has all of the fields we expect
   639  		checked := 0
   640  		for _, b := range gotBundles {
   641  			if b.CsvName != "etcdoperator.v0.9.2" {
   642  				continue
   643  			}
   644  			if b.ChannelName == "stable" {
   645  				EqualBundles(t, *etcdStable, *b)
   646  				checked++
   647  			}
   648  			if b.ChannelName == "alpha" {
   649  				EqualBundles(t, *etcdAlpha, *b)
   650  				checked++
   651  			}
   652  		}
   653  		require.Equal(2, checked)
   654  	}
   655  }
   656  
   657  func EqualBundles(t *testing.T, expected, actual api.Bundle) {
   658  	t.Helper()
   659  	stripPlural(actual.ProvidedApis)
   660  	stripPlural(actual.RequiredApis)
   661  
   662  	require.ElementsMatch(t, expected.ProvidedApis, actual.ProvidedApis, "provided apis don't match: %#v\n%#v", expected.ProvidedApis, actual.ProvidedApis)
   663  	require.ElementsMatch(t, expected.RequiredApis, actual.RequiredApis, "required apis don't match: %#v\n%#v", expected.RequiredApis, actual.RequiredApis)
   664  	require.ElementsMatch(t, expected.Dependencies, actual.Dependencies, "dependencies don't match: %#v\n%#v", expected.Dependencies, actual.Dependencies)
   665  	require.ElementsMatch(t, expected.Properties, actual.Properties, "properties don't match: %#v\n%#v", expected.Properties, actual.Properties)
   666  	require.ElementsMatch(t, expected.Object, actual.Object, "objects don't match: %#v\n%#v", expected.Object, actual.Object)
   667  
   668  	expected.RequiredApis, expected.ProvidedApis, actual.RequiredApis, actual.ProvidedApis = nil, nil, nil, nil
   669  	expected.Dependencies, expected.Properties, actual.Dependencies, actual.Properties = nil, nil, nil, nil
   670  	expected.Object, actual.Object = nil, nil
   671  
   672  	opts := []cmp.Option{
   673  		cmpopts.IgnoreUnexported(api.Bundle{}),
   674  		cmpopts.IgnoreUnexported(api.GroupVersionKind{}),
   675  		cmpopts.IgnoreUnexported(api.Property{}),
   676  		cmpopts.IgnoreUnexported(api.Dependency{}),
   677  	}
   678  
   679  	require.Truef(t, cmp.Equal(expected, actual, opts...), cmp.Diff(expected, actual, opts...))
   680  }
   681  
   682  func stripPlural(gvks []*api.GroupVersionKind) {
   683  	for i := range gvks {
   684  		gvks[i].Plural = ""
   685  	}
   686  }
   687  
   688  func etcdoperator_v0_9_2(channel string, addSkipsReplaces, addExtraProperties bool) *api.Bundle {
   689  	b := &api.Bundle{
   690  		CsvName:     "etcdoperator.v0.9.2",
   691  		PackageName: "etcd",
   692  		ChannelName: channel,
   693  		CsvJson:     "{\"apiVersion\":\"operators.coreos.com/v1alpha1\",\"kind\":\"ClusterServiceVersion\",\"metadata\":{\"annotations\":{\"alm-examples\":\"[{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdCluster\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"size\\\":3,\\\"version\\\":\\\"3.2.13\\\"}},{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdRestore\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example-etcd-cluster\\\"},\\\"spec\\\":{\\\"etcdCluster\\\":{\\\"name\\\":\\\"example-etcd-cluster\\\"},\\\"backupStorageType\\\":\\\"S3\\\",\\\"s3\\\":{\\\"path\\\":\\\"\\u003cfull-s3-path\\u003e\\\",\\\"awsSecret\\\":\\\"\\u003caws-secret\\u003e\\\"}}},{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdBackup\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example-etcd-cluster-backup\\\"},\\\"spec\\\":{\\\"etcdEndpoints\\\":[\\\"\\u003cetcd-cluster-endpoints\\u003e\\\"],\\\"storageType\\\":\\\"S3\\\",\\\"s3\\\":{\\\"path\\\":\\\"\\u003cfull-s3-path\\u003e\\\",\\\"awsSecret\\\":\\\"\\u003caws-secret\\u003e\\\"}}}]\",\"olm.properties\":\"[{\\\"type\\\":\\\"other\\\",\\\"value\\\":{\\\"its\\\":\\\"notdefined\\\"}},{\\\"type\\\":\\\"olm.label\\\",\\\"value\\\":{\\\"label\\\":\\\"testlabel\\\"}},{\\\"type\\\":\\\"olm.label\\\",\\\"value\\\":{\\\"label\\\":\\\"testlabel1\\\"}}]\",\"olm.skipRange\":\"\\u003c 0.6.0\",\"tectonic-visibility\":\"ocs\"},\"name\":\"etcdoperator.v0.9.2\",\"namespace\":\"placeholder\"},\"spec\":{\"customresourcedefinitions\":{\"owned\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]},{\"description\":\"Limits describes the minimum/maximum amount of compute resources required/allowed\",\"displayName\":\"Resource Requirements\",\"path\":\"pod.resources\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:resourceRequirements\"]}],\"statusDescriptors\":[{\"description\":\"The status of each of the member Pods for the etcd cluster.\",\"displayName\":\"Member Status\",\"path\":\"members\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podStatuses\"]},{\"description\":\"The service at which the running etcd cluster can be accessed.\",\"displayName\":\"Service\",\"path\":\"serviceName\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Service\"]},{\"description\":\"The current size of the etcd cluster.\",\"displayName\":\"Cluster Size\",\"path\":\"size\"},{\"description\":\"The current version of the etcd cluster.\",\"displayName\":\"Current Version\",\"path\":\"currentVersion\"},{\"description\":\"The target version of the etcd cluster, after upgrading.\",\"displayName\":\"Target Version\",\"path\":\"targetVersion\"},{\"description\":\"The current status of the etcd cluster.\",\"displayName\":\"Status\",\"path\":\"phase\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase\"]},{\"description\":\"Explanation for the current status of the cluster.\",\"displayName\":\"Status Details\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to backup an etcd cluster.\",\"displayName\":\"etcd Backup\",\"kind\":\"EtcdBackup\",\"name\":\"etcdbackups.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"Specifies the endpoints of an etcd cluster.\",\"displayName\":\"etcd Endpoint(s)\",\"path\":\"etcdEndpoints\",\"x-descriptors\":[\"urn:alm:descriptor:etcd:endpoint\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the backup was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any backup related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to restore an etcd cluster from a backup.\",\"displayName\":\"etcd Restore\",\"kind\":\"EtcdRestore\",\"name\":\"etcdrestores.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"References the EtcdCluster which should be restored,\",\"displayName\":\"etcd Cluster\",\"path\":\"etcdCluster.name\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:EtcdCluster\",\"urn:alm:descriptor:text\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the restore was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any restore related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"}],\"required\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]}],\"version\":\"v1beta2\"}]},\"description\":\"etcd is a distributed key value store that provides a reliable way to store data across a cluster of machines. It’s open-source and available on GitHub. etcd gracefully handles leader elections during network partitions and will tolerate machine failure, including the leader. Your applications can read and write data into etcd.\\nA simple use-case is to store database connection details or feature flags within etcd as key value pairs. These values can be watched, allowing your app to reconfigure itself when they change. Advanced uses take advantage of the consistency guarantees to implement database leader elections or do distributed locking across a cluster of workers.\\n\\n_The etcd Open Cloud Service is Public Alpha. The goal before Beta is to fully implement backup features._\\n\\n### Reading and writing to etcd\\n\\nCommunicate with etcd though its command line utility `etcdctl` or with the API using the automatically generated Kubernetes Service.\\n\\n[Read the complete guide to using the etcd Open Cloud Service](https://coreos.com/tectonic/docs/latest/alm/etcd-ocs.html)\\n\\n### Supported Features\\n\\n\\n**High availability**\\n\\n\\nMultiple instances of etcd are networked together and secured. Individual failures or networking issues are transparently handled to keep your cluster up and running.\\n\\n\\n**Automated updates**\\n\\n\\nRolling out a new etcd version works like all Kubernetes rolling updates. Simply declare the desired version, and the etcd service starts a safe rolling update to the new version automatically.\\n\\n\\n**Backups included**\\n\\n\\nComing soon, the ability to schedule backups to happen on or off cluster.\\n\",\"displayName\":\"etcd\",\"icon\":[{\"base64data\":\"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\",\"mediatype\":\"image/png\"}],\"install\":{\"spec\":{\"deployments\":[{\"name\":\"etcd-operator\",\"spec\":{\"replicas\":1,\"selector\":{\"matchLabels\":{\"name\":\"etcd-operator-alm-owned\"}},\"template\":{\"metadata\":{\"labels\":{\"name\":\"etcd-operator-alm-owned\"},\"name\":\"etcd-operator-alm-owned\"},\"spec\":{\"containers\":[{\"command\":[\"etcd-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-operator\"},{\"command\":[\"etcd-backup-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-backup-operator\"},{\"command\":[\"etcd-restore-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-restore-operator\"}],\"serviceAccountName\":\"etcd-operator\"}}}}],\"permissions\":[{\"rules\":[{\"apiGroups\":[\"etcd.database.coreos.com\"],\"resources\":[\"etcdclusters\",\"etcdbackups\",\"etcdrestores\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"pods\",\"services\",\"endpoints\",\"persistentvolumeclaims\",\"events\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"apps\"],\"resources\":[\"deployments\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"secrets\"],\"verbs\":[\"get\"]}],\"serviceAccountName\":\"etcd-operator\"}]},\"strategy\":\"deployment\"},\"keywords\":[\"etcd\",\"key value\",\"database\",\"coreos\",\"open source\"],\"labels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"},\"links\":[{\"name\":\"Blog\",\"url\":\"https://coreos.com/etcd\"},{\"name\":\"Documentation\",\"url\":\"https://coreos.com/operators/etcd/docs/latest/\"},{\"name\":\"etcd Operator Source Code\",\"url\":\"https://github.com/coreos/etcd-operator\"}],\"maintainers\":[{\"email\":\"support@coreos.com\",\"name\":\"CoreOS, Inc\"}],\"maturity\":\"alpha\",\"provider\":{\"name\":\"CoreOS, Inc\"},\"relatedImages\":[{\"image\":\"quay.io/coreos/etcd@sha256:3816b6daf9b66d6ced6f0f966314e2d4f894982c6b1493061502f8c2bf86ac84\",\"name\":\"etcd-v3.4.0\"},{\"image\":\"quay.io/coreos/etcd@sha256:49d3d4a81e0d030d3f689e7167f23e120abf955f7d08dbedf3ea246485acee9f\",\"name\":\"etcd-3.4.1\"}],\"replaces\":\"etcdoperator.v0.9.0\",\"selector\":{\"matchLabels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"}},\"skips\":[\"etcdoperator.v0.9.1\"],\"version\":\"0.9.2\"}}",
   694  		Object: []string{
   695  			"{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"name\":\"etcdbackups.etcd.database.coreos.com\"},\"spec\":{\"group\":\"etcd.database.coreos.com\",\"names\":{\"kind\":\"EtcdBackup\",\"listKind\":\"EtcdBackupList\",\"plural\":\"etcdbackups\",\"singular\":\"etcdbackup\"},\"scope\":\"Namespaced\",\"version\":\"v1beta2\"}}",
   696  			"{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"name\":\"etcdclusters.etcd.database.coreos.com\"},\"spec\":{\"group\":\"etcd.database.coreos.com\",\"names\":{\"kind\":\"EtcdCluster\",\"listKind\":\"EtcdClusterList\",\"plural\":\"etcdclusters\",\"shortNames\":[\"etcdclus\",\"etcd\"],\"singular\":\"etcdcluster\"},\"scope\":\"Namespaced\",\"version\":\"v1beta2\"}}",
   697  			"{\"apiVersion\":\"operators.coreos.com/v1alpha1\",\"kind\":\"ClusterServiceVersion\",\"metadata\":{\"annotations\":{\"alm-examples\":\"[{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdCluster\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"size\\\":3,\\\"version\\\":\\\"3.2.13\\\"}},{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdRestore\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example-etcd-cluster\\\"},\\\"spec\\\":{\\\"etcdCluster\\\":{\\\"name\\\":\\\"example-etcd-cluster\\\"},\\\"backupStorageType\\\":\\\"S3\\\",\\\"s3\\\":{\\\"path\\\":\\\"\\u003cfull-s3-path\\u003e\\\",\\\"awsSecret\\\":\\\"\\u003caws-secret\\u003e\\\"}}},{\\\"apiVersion\\\":\\\"etcd.database.coreos.com/v1beta2\\\",\\\"kind\\\":\\\"EtcdBackup\\\",\\\"metadata\\\":{\\\"name\\\":\\\"example-etcd-cluster-backup\\\"},\\\"spec\\\":{\\\"etcdEndpoints\\\":[\\\"\\u003cetcd-cluster-endpoints\\u003e\\\"],\\\"storageType\\\":\\\"S3\\\",\\\"s3\\\":{\\\"path\\\":\\\"\\u003cfull-s3-path\\u003e\\\",\\\"awsSecret\\\":\\\"\\u003caws-secret\\u003e\\\"}}}]\",\"olm.properties\":\"[{\\\"type\\\":\\\"other\\\",\\\"value\\\":{\\\"its\\\":\\\"notdefined\\\"}},{\\\"type\\\":\\\"olm.label\\\",\\\"value\\\":{\\\"label\\\":\\\"testlabel\\\"}},{\\\"type\\\":\\\"olm.label\\\",\\\"value\\\":{\\\"label\\\":\\\"testlabel1\\\"}}]\",\"olm.skipRange\":\"\\u003c 0.6.0\",\"tectonic-visibility\":\"ocs\"},\"name\":\"etcdoperator.v0.9.2\",\"namespace\":\"placeholder\"},\"spec\":{\"customresourcedefinitions\":{\"owned\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]},{\"description\":\"Limits describes the minimum/maximum amount of compute resources required/allowed\",\"displayName\":\"Resource Requirements\",\"path\":\"pod.resources\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:resourceRequirements\"]}],\"statusDescriptors\":[{\"description\":\"The status of each of the member Pods for the etcd cluster.\",\"displayName\":\"Member Status\",\"path\":\"members\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podStatuses\"]},{\"description\":\"The service at which the running etcd cluster can be accessed.\",\"displayName\":\"Service\",\"path\":\"serviceName\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Service\"]},{\"description\":\"The current size of the etcd cluster.\",\"displayName\":\"Cluster Size\",\"path\":\"size\"},{\"description\":\"The current version of the etcd cluster.\",\"displayName\":\"Current Version\",\"path\":\"currentVersion\"},{\"description\":\"The target version of the etcd cluster, after upgrading.\",\"displayName\":\"Target Version\",\"path\":\"targetVersion\"},{\"description\":\"The current status of the etcd cluster.\",\"displayName\":\"Status\",\"path\":\"phase\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase\"]},{\"description\":\"Explanation for the current status of the cluster.\",\"displayName\":\"Status Details\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to backup an etcd cluster.\",\"displayName\":\"etcd Backup\",\"kind\":\"EtcdBackup\",\"name\":\"etcdbackups.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"Specifies the endpoints of an etcd cluster.\",\"displayName\":\"etcd Endpoint(s)\",\"path\":\"etcdEndpoints\",\"x-descriptors\":[\"urn:alm:descriptor:etcd:endpoint\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the backup was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any backup related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"},{\"description\":\"Represents the intent to restore an etcd cluster from a backup.\",\"displayName\":\"etcd Restore\",\"kind\":\"EtcdRestore\",\"name\":\"etcdrestores.etcd.database.coreos.com\",\"specDescriptors\":[{\"description\":\"References the EtcdCluster which should be restored,\",\"displayName\":\"etcd Cluster\",\"path\":\"etcdCluster.name\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:EtcdCluster\",\"urn:alm:descriptor:text\"]},{\"description\":\"The full AWS S3 path where the backup is saved.\",\"displayName\":\"S3 Path\",\"path\":\"s3.path\",\"x-descriptors\":[\"urn:alm:descriptor:aws:s3:path\"]},{\"description\":\"The name of the secret object that stores the AWS credential and config files.\",\"displayName\":\"AWS Secret\",\"path\":\"s3.awsSecret\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes:Secret\"]}],\"statusDescriptors\":[{\"description\":\"Indicates if the restore was successful.\",\"displayName\":\"Succeeded\",\"path\":\"succeeded\",\"x-descriptors\":[\"urn:alm:descriptor:text\"]},{\"description\":\"Indicates the reason for any restore related failures.\",\"displayName\":\"Reason\",\"path\":\"reason\",\"x-descriptors\":[\"urn:alm:descriptor:io.kubernetes.phase:reason\"]}],\"version\":\"v1beta2\"}],\"required\":[{\"description\":\"Represents a cluster of etcd nodes.\",\"displayName\":\"etcd Cluster\",\"kind\":\"EtcdCluster\",\"name\":\"etcdclusters.etcd.database.coreos.com\",\"resources\":[{\"kind\":\"Service\",\"version\":\"v1\"},{\"kind\":\"Pod\",\"version\":\"v1\"}],\"specDescriptors\":[{\"description\":\"The desired number of member Pods for the etcd cluster.\",\"displayName\":\"Size\",\"path\":\"size\",\"x-descriptors\":[\"urn:alm:descriptor:com.tectonic.ui:podCount\"]}],\"version\":\"v1beta2\"}]},\"description\":\"etcd is a distributed key value store that provides a reliable way to store data across a cluster of machines. It’s open-source and available on GitHub. etcd gracefully handles leader elections during network partitions and will tolerate machine failure, including the leader. Your applications can read and write data into etcd.\\nA simple use-case is to store database connection details or feature flags within etcd as key value pairs. These values can be watched, allowing your app to reconfigure itself when they change. Advanced uses take advantage of the consistency guarantees to implement database leader elections or do distributed locking across a cluster of workers.\\n\\n_The etcd Open Cloud Service is Public Alpha. The goal before Beta is to fully implement backup features._\\n\\n### Reading and writing to etcd\\n\\nCommunicate with etcd though its command line utility `etcdctl` or with the API using the automatically generated Kubernetes Service.\\n\\n[Read the complete guide to using the etcd Open Cloud Service](https://coreos.com/tectonic/docs/latest/alm/etcd-ocs.html)\\n\\n### Supported Features\\n\\n\\n**High availability**\\n\\n\\nMultiple instances of etcd are networked together and secured. Individual failures or networking issues are transparently handled to keep your cluster up and running.\\n\\n\\n**Automated updates**\\n\\n\\nRolling out a new etcd version works like all Kubernetes rolling updates. Simply declare the desired version, and the etcd service starts a safe rolling update to the new version automatically.\\n\\n\\n**Backups included**\\n\\n\\nComing soon, the ability to schedule backups to happen on or off cluster.\\n\",\"displayName\":\"etcd\",\"icon\":[{\"base64data\":\"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\",\"mediatype\":\"image/png\"}],\"install\":{\"spec\":{\"deployments\":[{\"name\":\"etcd-operator\",\"spec\":{\"replicas\":1,\"selector\":{\"matchLabels\":{\"name\":\"etcd-operator-alm-owned\"}},\"template\":{\"metadata\":{\"labels\":{\"name\":\"etcd-operator-alm-owned\"},\"name\":\"etcd-operator-alm-owned\"},\"spec\":{\"containers\":[{\"command\":[\"etcd-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-operator\"},{\"command\":[\"etcd-backup-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-backup-operator\"},{\"command\":[\"etcd-restore-operator\",\"--create-crd=false\"],\"env\":[{\"name\":\"MY_POD_NAMESPACE\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.namespace\"}}},{\"name\":\"MY_POD_NAME\",\"valueFrom\":{\"fieldRef\":{\"fieldPath\":\"metadata.name\"}}}],\"image\":\"quay.io/coreos/etcd-operator@sha256:c0301e4686c3ed4206e370b42de5a3bd2229b9fb4906cf85f3f30650424abec2\",\"name\":\"etcd-restore-operator\"}],\"serviceAccountName\":\"etcd-operator\"}}}}],\"permissions\":[{\"rules\":[{\"apiGroups\":[\"etcd.database.coreos.com\"],\"resources\":[\"etcdclusters\",\"etcdbackups\",\"etcdrestores\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"pods\",\"services\",\"endpoints\",\"persistentvolumeclaims\",\"events\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"apps\"],\"resources\":[\"deployments\"],\"verbs\":[\"*\"]},{\"apiGroups\":[\"\"],\"resources\":[\"secrets\"],\"verbs\":[\"get\"]}],\"serviceAccountName\":\"etcd-operator\"}]},\"strategy\":\"deployment\"},\"keywords\":[\"etcd\",\"key value\",\"database\",\"coreos\",\"open source\"],\"labels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"},\"links\":[{\"name\":\"Blog\",\"url\":\"https://coreos.com/etcd\"},{\"name\":\"Documentation\",\"url\":\"https://coreos.com/operators/etcd/docs/latest/\"},{\"name\":\"etcd Operator Source Code\",\"url\":\"https://github.com/coreos/etcd-operator\"}],\"maintainers\":[{\"email\":\"support@coreos.com\",\"name\":\"CoreOS, Inc\"}],\"maturity\":\"alpha\",\"provider\":{\"name\":\"CoreOS, Inc\"},\"relatedImages\":[{\"image\":\"quay.io/coreos/etcd@sha256:3816b6daf9b66d6ced6f0f966314e2d4f894982c6b1493061502f8c2bf86ac84\",\"name\":\"etcd-v3.4.0\"},{\"image\":\"quay.io/coreos/etcd@sha256:49d3d4a81e0d030d3f689e7167f23e120abf955f7d08dbedf3ea246485acee9f\",\"name\":\"etcd-3.4.1\"}],\"replaces\":\"etcdoperator.v0.9.0\",\"selector\":{\"matchLabels\":{\"alm-owner-etcd\":\"etcdoperator\",\"operated-by\":\"etcdoperator\"}},\"skips\":[\"etcdoperator.v0.9.1\"],\"version\":\"0.9.2\"}}",
   698  			"{\"apiVersion\":\"apiextensions.k8s.io/v1beta1\",\"kind\":\"CustomResourceDefinition\",\"metadata\":{\"name\":\"etcdrestores.etcd.database.coreos.com\"},\"spec\":{\"group\":\"etcd.database.coreos.com\",\"names\":{\"kind\":\"EtcdRestore\",\"listKind\":\"EtcdRestoreList\",\"plural\":\"etcdrestores\",\"singular\":\"etcdrestore\"},\"scope\":\"Namespaced\",\"version\":\"v1beta2\"}}",
   699  		},
   700  		BundlePath: "",
   701  		Dependencies: []*api.Dependency{
   702  			{
   703  				Type:  "olm.gvk",
   704  				Value: `{"group":"etcd.database.coreos.com","kind":"EtcdCluster","version":"v1beta2"}`,
   705  			},
   706  		},
   707  		Properties: []*api.Property{
   708  			{
   709  				Type:  "olm.package",
   710  				Value: `{"packageName":"etcd","version":"0.9.2"}`,
   711  			},
   712  			{
   713  				Type:  "olm.gvk",
   714  				Value: `{"group":"etcd.database.coreos.com","kind":"EtcdCluster","version":"v1beta2"}`,
   715  			},
   716  			{
   717  				Type:  "olm.gvk",
   718  				Value: `{"group":"etcd.database.coreos.com","kind":"EtcdRestore","version":"v1beta2"}`,
   719  			},
   720  			{
   721  				Type:  "olm.gvk",
   722  				Value: `{"group":"etcd.database.coreos.com","kind":"EtcdBackup","version":"v1beta2"}`,
   723  			},
   724  			{
   725  				Type:  "olm.label",
   726  				Value: `{"label":"testlabel"}`,
   727  			},
   728  			{
   729  				Type:  "olm.label",
   730  				Value: `{"label":"testlabel1"}`,
   731  			},
   732  			{
   733  				Type:  "other",
   734  				Value: `{"its":"notdefined"}`,
   735  			},
   736  		},
   737  		ProvidedApis: []*api.GroupVersionKind{
   738  			{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdCluster"},
   739  			{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdBackup"},
   740  			{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdRestore"},
   741  		},
   742  		RequiredApis: []*api.GroupVersionKind{
   743  			{Group: "etcd.database.coreos.com", Version: "v1beta2", Kind: "EtcdCluster"},
   744  		},
   745  		Version:   "0.9.2",
   746  		SkipRange: "< 0.6.0",
   747  	}
   748  	if addSkipsReplaces {
   749  		b.Replaces = "etcdoperator.v0.9.0"
   750  		b.Skips = []string{"etcdoperator.v0.9.1"}
   751  	}
   752  	if addExtraProperties {
   753  		b.Properties = append(b.Properties, []*api.Property{
   754  			{Type: "olm.gvk.required", Value: `{"group":"etcd.database.coreos.com","kind":"EtcdCluster","version":"v1beta2"}`},
   755  		}...)
   756  	}
   757  	return b
   758  }